diff --git a/.gitignore b/.gitignore index 8ab9a21145b7911a0b893fac2ed3121ceba4ebcb..bbe8a2da8ce9a7805ce0535cd72687832f94ba2b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,10 @@ src/src # Параметры VSCodium .vscode + +/target +/libcport/test.db +/test.txt +/**/Cargo.lock +/**/mkport.conf +/cport_cli/src/bin/update_metadata.rs diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 5df688df0c1b42354be4df1144d7903a71f5c918..0000000000000000000000000000000000000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,54 +0,0 @@ -# Change Log - -## cport v1.0current - -- 02.10.2022 cov_id111 - - Изменение логики `cport_cli.install.Install.install()` - теперь, если порт найден в БД или ФС, установка не завершается с ошибкой, а порт переустанавливается; - - Перенос `cport_cli.update.main()` в `cport_cli.args_parser.update()`; - - Добавление обработки ситуации, когда в параметрах `port.toml` порта указаны некорректные сведения; - - Добавление функции для очистки кеша; - - Добавление обработки ситуации, при которой отсутствует `files.list` порта - метод `cport_cli.install.Install.install()`; - - Работа над debug-сообщениями cport; - - Фиксы условий `shared.sh`; - - Добавление метода `libcport.core.get_files_in_cache()` для получения количества файлов в кеше. - -- 01.10.2022 cov_id111 - - Добавление проверки наличия порта в файловой системе; - - Метод `libcport.install.check()` помечен как [DEPRECATED] ввиду наличия аналога `libcport.core.check()`; - - Оптимизация метода `libcport.core.CDatabase.create_initial_db()`. - -- 29.09.2022 cov_id111 - - Добавление поддержки опционального параметра `port.file` файла `port.toml` порта; - - Рефакторинг `cport_cl.install`, `cport_cli.info`. - -- 24.09.2022 cov_id111 - - Мелкие изменения сообщений в `cport_cli.{info.py,shared.sh}`. - -- 18.09.2022 cov_id111 - - Реорганизация аргументов командной строки; - - Удаление метода `libcport.core.CDatabase._close_db()`; - - Добавление операций по закрытию БД после её использования; - - Добавление констант `libcport.constants.PORT_TOML_PARAMS` и `libcport.constants.PORT_TOML_STATUSES`: - - первая содержит `dict` со списком необходимых параметров в `port.toml` порта; - - вторая содержит статусы секций `packages`, `deps` и `port` в этом файле: - - `True` - обязательный; - - `False` - необязательный. - -- 17.09.2022 cov_id111 - - Добавление пасхалки в `cport list -b`; - -## cport v1.0b1 - -- 17.09.2022 cov_id111 - - ***cport v1.0b1 released!*** - - [DOC] Обновление GIF в `README.md`; - - [DOC] Актуализация сведений об использовании; - - Реорганизация использования аргументов командной строки: - - добавление группы опций `list` для просмотра списка портов. - - Добавление сортировки списка портов: - - все порты, без сортировки. - - все порты, с сортировкой. - - все установленные порты. - - все НЕ установленные порты. - - все битые порты. - - Добавление класса `libcport.core.CList()`. \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..0374cffc3944dadc16d1e71249736fbe881e6f0f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,48 @@ +[workspace] +resolver = "2" +members = ["libcport", "cport_cli", "mkport", "bs", "libportutil", "pu", "poc"] + +[workspace.dependencies] +anyhow = "1" +clap = { version = "4", features = ["derive"] } +colored = "2" +cursive = "0.20" +libmsg = { git = "https://gitlab.com/calmiralinux/libmsg" } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +chrono = "0.4" +futures = "0.3" +futures-util = "0.3" +git2 = "0.18.1" +indicatif = "0.17" +rusqlite = "0.29" +thiserror = "1" +tokio = { version = "1", features = ["full"] } +toml = "0.8" +log = "0.4" +log4rs = "1" +reqwest = { version = "0.11", features = ["stream"] } +walkdir = "2" +home = "0.5" +env_logger = "0.10" +num_cpus = "1" +console = "0.15" +tabled = { version = "0.14", features = ["color"] } +sha256 = "1" +md5 = "0.7" +ron = "0.8" +which = "4" +i18n-embed = { version = "0.14", features = [ + "fluent-system", + "desktop-requester", +] } +i18n-embed-fl = "0.7" +lazy_static = "1.4" +rust-embed = "8" + +[profile.release] +lto = true +strip = true +debug = false +opt-level = "s" +panic = "abort" diff --git a/Makefile b/Makefile deleted file mode 100644 index 8b34d9252fdf9f2bac40a948351c48cb7a97ed49..0000000000000000000000000000000000000000 --- a/Makefile +++ /dev/null @@ -1,34 +0,0 @@ -DESTDIR=/ -PYVERSION="3.10" - -binary: - python3 -m nuitka --low-memory --onefile src/cport.py - -install-binary: - mkdir -p ${DESTDIR}usr/bin - mkdir -p ${DESTDIR}usr/share/cport - mkdir -p ${DESTDIR}var/lib/Calmira - mkdir -p ${DESTDIR}var/cache/cport/downloads - cp -v cport.bin ${DESTDIR}usr/bin/cport - cp -v src/cport_cli/shared.sh ${DESTDIR}usr/share/cport - -remove-binary: - rm -v ${DESTDIR}usr/bin/cport - -clean: - rm -rf cport.{build,dist,onefile-build} cport.bin - -install: - mkdir -pv /usr/lib/python${PYVERSION}/site-packages - mkdir -p ${DESTDIR}usr/lib/python${PYVERSION}/site-packages - mkdir -p ${DESTDIR}usr/bin - mkdir -p ${DESTDIR}var/lib/Calmira - mkdir -p ${DESTDIR}usr/share/cport - mkdir -p ${DESTDIR}var/cache/cport/downloads - cp -rv src/{cport_cli,libcport} ${DESTDIR}usr/lib/python${PYVERSION}/site-packages/ - cp -rv src/cport_cli/shared.sh ${DESTDIR}usr/share/cport - cp -v src/cport.py ${DESTDIR}usr/bin/cport - -remove: - rm -rvf ${DESTDIR}/usr/{bin/cport,lib/python${PYVERSION}/site-packages/{cport_cli,libcport}} - rm -rvf ${DESTDIR}/var/{cache/cport,lib/Calmira} diff --git a/README.md b/README.md index 165da5356dc9462298c8c37d5b5965ee6fdf0b99..e1e76fa458e58f2368dbfe462f2a24de91e18fcb 100644 --- a/README.md +++ b/README.md @@ -1,244 +1,38 @@ # cport -![](pic/cport_check.gif) - -cport - менеджер системы портов дистрибутива Calmira GNU/Linux-libre, ставящий -перед собой задачу упрощения работы с программным обеспечением в этом -дистрибутиве. - -## Содержание - -[TOC] - -## Предназначение - -У cport несколько целей: - -1. **Демонстрационная** - программа предназначена для демонстрации моделей - управления программным обеспечением в операционных системах семейства - GNU/Linux. В данном случае она демонстрирует использование системы портов как - метода для автоматизации сборки ПО из исходного кода. -2. **Практическая** - помимо автоматизации процессов, связанных с управлением - ПО, собранным из исходного кода, cport является утилитой для упращения УПО в - дистрибутивах GNU/Linux, собранных по LFS. cport ведёт базу данных ПО, а - также умеет выполнять базовые задачи по обслуживанию ОС. - -## Сборка - -### Зависимости - -| Тип | Порт/пакет | -| ------------ | ------------------------------------------------------------------------------------------------------------------ | -| Необходимые |
  1. base/python
  2. base/pymo/wget
  3. base/pymo/toml
| -| Опциональные |
  1. base/gcc
  2. general/pymo/nuitka
| - -### Использование Python-скриптов - -```bash -make install -``` - -### Трансляция Python-кода в C-код и компиляция посредством GCC - -```bash -make binary -make install-binary -``` - -## Синтаксис - -```bash -cport [-h] [--install NAME_I [NAME_I ...]] [--remove NAME_R [NAME_R ...]] - [--info NAME_I [NAME_I ...]] [--update] [--upgrade NAME_U [NAME_U ...]] - [--is-exist NAME_E] [--is-installed NAME_INS] [--check-ports] - [--yes] [--ignore-db IGNORE_DB] [--skip-check] [--rebuild-db] [-v] - {list} ... - -cport list [-h] [-a] [-i] [-I] [-b] -``` - -## Использование - -- `-i`, `--install` - установить порт(ы); -- `-r`, `--remove` - удалить порт(ы); -- `-u`, `--update` - обновить систему портов; -- `-I`, `--info` - просмотреть информацию о порте(ах); -- `list` - просмотреть список портов: - - `-a`, `--all` - всех портов (с отображением, установлены они или нет); - - `-i`, `--installed` - только установленных; - - `-I`, `--not-installed` - только НЕ установленных; - - `-b`, `--broken` - только битых; -- `--is-installed` - проверить, установлен ли порт или нет; -- `--is-exist` - проверить, существует ли порт; -- `-y`, `--yes` - отвечать утвердительно на все операции, запрашивающие у - пользователя подтверждения (при установке и удалении, например); -- `--rebuild-db` - восстановить/обновить/пересобрать базу данных cport; -- `--check-ports` - проверить систему портов на наличие или отсутствие портов, - указанных в качестве зависимостей других портов; -- `--ignore-db` - пропускать обращения к базе данных в операциях для установки и - удаления ПО. - -Например: - -```bash -# Установка портов 'editors/gvim', 'editors/nano' и 'base/acl' -# Соотв., будут установлены GVim, Nano и ACL. -cport -i editors/gvim editors/nano base/acl - -# Удаление 'editors/gvim' -cport -r editors/gvim - -# Просмотр информации об 'editors/gvim' -cport -I editors/gvim -``` - -Для просмотра доп. сведений введите: - -```bash -cport -h -``` - -### Установка порта - -Для того, чтобы установить порт в систему, используется ключ `--install` или его -сокращённая версия `-i`: - -```bash -cport --install [все порты, разделённые между собой пробелом] -``` - -cport проверяет наличие порта в базе данных. В том случае, если порта в БД нет, -то выполняет сборочные инструкции из файла `install` порта. В случае, если ПО в -БД присутствует, то завершает работу с ошибкой. - -В том случае, если сборочные инструкции в файле `install` отработали без ошибок, -cport вычисляет статус установки порта. Статус - это число от 0 до 3, означающее -количество установленных в систему файлов (список файлов берётся из `files.list` -порта): - -- **0** - 100% файлов установлено в систему; -- **1** - 75%-99% файлов установлено в систему; -- **2** - 50-74% файлов; -- **3** - 5-49% файлов. - -Вычисленная информация вместе с остальной информацией о порте, взятой из файла -`port.toml`, добавляется в базу данных. В том случае, если статус равен 4 (не -считается статусом, но всё равно получается при вычислении), т.е. в системе -присутствует менее 4% или 5%, то считается, что установка порта произведена -неудачно. Порт в базу данных не добавляется и не считается установленным в -систему. - -### Удаление порта - -Для удаления порта из файловой системы используется ключ `--remove` или его -сокрущённая версия `-r`. - -cport проверяет наличие порта в базе данных. В том случае, если там он -отсутствует, то работа завершается с ошибкой. В противном случае cport читает -содержимое `files.list`, где записан список файлов порта, которые должны -присутствовать в системе и последовательно удаляет их из директории `/*`. После -чего удаляет запись о порте из базы данных. - -### Просмотр информации - -Для того, чтобы просмотреть информацию о программном обеспечении, которое -находится в системе портов, используется ключ `--info` или его сокращённая -версия `-I`: - -```bash -cport --info [список портов, разделённых между собой ПРОБЕЛОМ] -``` - -![](pic/cport_info.gif) - -Данные выводятся из файла `port.toml` порта (конкретно - из секций `package` и -`deps`). Кроме того проверяется, установлен ли порт в систему. - -### Просмотр списка всех доступных портов - -Для того, чтобы просмотреть список всех доступных портов, используется опция -`list`. Это может не только вывести список -всего ПО, но ещё и выполнить первичную диагностику системы портов, так как при -выводе списка портов cport проверяет каждый из них на корректность: - -- Наличие нужных файлов в директории порта (`install`, `port.toml`, - `files.list`); -- Корректность конфига `port.toml`; -- Наличие директории порта в файловой системе `/usr/ports`. - -Кроме того, cport выделяет цветом каждый из портов. Первый символ вывода окрашен -в один из трёх цветов: - -1. **Зелёный:** порт присутствует в базе данных; -2. **Красный:** порт некорректен (возможно, в нём отсутствуют какие-либо файлы, - либо конфиг `port.toml` создан с ошибками); -3. **Серый:** порт полностью корректен, отсутствует в базе данных cport. - -![](pic/cport_list.gif) - -#### Ключи - -- `-a` - просмотр всех портов (также указывает, установлен ли порт или нет); -- `-i` - просмотр только установленных портов; -- `-I` - просмотр только НЕ установленных портов; -- `-b` - просмотр только битых портов. - -В том случае, если опции `list` не передан никакой из выше перечисленных ключей, -то будет выведен список _всех_ портов БЕЗ сортировки или цветового выделения. - -> **Note** - -> Здесь были показаны только основные сведения. Для просмотра более подробной -> информации обо всех возможностях cport, пожалуйста, обратитесь к -> [**документации**](docs/README.md). - -## Установка cport в Calmira GNU/Linux(-libre) - -Для установки нужны следующие зависимости: - -- `base/sqlite3` - `cport` использует базу данных SQLite3 для хранения - информации о портах; -- `base/pymo/wget` - используется для скачивания архивов с исходным кодом ПО. - -### Совместимость - -Совместимо со следующими релизами Calmira: - -- v2.0; - -## ROADMAP - -- [x] Разработка основных методов для установки порта, его удаления и просмотра - о нём информации, а также обновления системы портов; - - [x] build and install ports; - - [x] remove ports; - - [x] information about ports; - - [x] update port system; - - [x] update software; -- [x] Разработка методов для диагностики системы портов: - - [x] list software; - - [ ] checking port system (port linter); - - [x] rebuild database; - -## Документация - -На данный момент ведутся работы над [документацией](docs/README.md) по `cport`, -однако они ещё не закончены. Вы можете внести свой вклад в документирование -этого программного обеспечения, то сделайте форк этого репозитория, напишите -нужные вам страницы в формате Markdown и отправьте нам Merge Request с -изменениями. Либо просто оставьте Issue о необходимости добавления какой-либо -страницы документации. Кроме того, нужно написать страницы документации man, так -как Manual Pages - это основной способ хранения и передачи документации в -UNIX-системах. - -## Стек технологий - -- **ЯП:** Python; -- **БД:** SQLite3; -- **ОС:** Calmira GNU/Linux-libre. - -## Лицензия - -Программное обеспечение распространяется под лицензией GNU GPLv3. - -![](pic/cport1.png) +`cport` is a set of utilities for working with the Calmira GNU/Linux-libre ports system. It provides a basic `libcport` API for interacting with ports and the whole port system, a `cport` program for automating software builds, updates and deletions and `mkport` for creating new ports. + +## Libs + +- [`libcport`](libcport/README.md) is a library for managing the Calmira GNU/Linux-libre ports system. It provides an API for installing and removing ports, maintaining them (creating, removing, updating) and getting information about ports. +- `libportutil` is a library for `pu` and `poc` programs containing similar functions from `libcport` crate. + +## Binary files + +- [`cport_cli`](cport_cli/README.md) (`cport`) is a wrapper over [`libcport`](libcport/README.md) which provides the user with a simple command line interface to control the software in Calmira GNU/Linux-libre. +- [`mkport`](mkport/README.md) - pseudographic utility for automating the creation of new ports. It creates a separate git branch in which to make all changes. It requests some of the necessary data from the user, and then creates the port automatically. +- [`pu`](pu/README.md) is the tool for updating existing ports. +- [`poc`](poc/README.md) (Port Outdate Checker) tool for checking the `port` latest version from the Arch Linux repository. + +## Progress + +- [ ] **`libcport`** + - [X] `libcport::port::Port` + - [X] install ports + - [X] remove ports + - [ ] upgrade ports + - [ ] upgrade PS + - [ ] database +- [ ] **`cport_cli`** + - [X] install ports + - [X] remove ports + - [X] info about ports + - [ ] upgrade ports + - [ ] upgrade PS + - [X] list ports +- [ ] **`mkport`** +- [ ] **`bs`** + +## License + +GNU GPLv3.0 or later. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index e8034f12a72e4cab4a80cd87650de5d302039b41..0000000000000000000000000000000000000000 --- a/TODO.md +++ /dev/null @@ -1,3 +0,0 @@ -# Обновление [ ] - -Не обрабатывается ситуация, когда отсутствует файл `/etc/calm-release` diff --git a/bs/Cargo.toml b/bs/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..24f1984a467c1356c324bc9c2cfbd27ae9c05cac --- /dev/null +++ b/bs/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "bs" +version = "0.3.0" +edition = "2021" +authors = ["Michail Krasnov "] +license = "GPL-3.0" +description = "Automated building system for Calmira GNU/Linux-libre" + +[dependencies] +libcport = { path = "../libcport" } +notify-rust = "4.9.0" + +serde.workspace = true +toml.workspace = true diff --git a/bs/README.md b/bs/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e761988ed31f0ba07d381d3672363a98747679e5 --- /dev/null +++ b/bs/README.md @@ -0,0 +1,19 @@ +# bs + +`bs` - Calmira GNU/Linux-libre distribution automated build system. + +## Usage + +```bash +bs -m MODE +``` + +`MODE` - режим работы системы сборки. Режимы работы описываются в одноимённом +файле с расширением `.toml`. Пример: + +```bash +# Создание файла с описанием режима работы +vim calmira_base_system.toml # Пользователь вводит какие-то данные... +# Выполнение сборочных инструкций +bs -m calmira_base_system +``` diff --git a/bs/assests/bs.toml b/bs/assests/bs.toml new file mode 100644 index 0000000000000000000000000000000000000000..512e790741570f1d5e98d4073489cbe59ae34aac --- /dev/null +++ b/bs/assests/bs.toml @@ -0,0 +1,10 @@ +[stages] +build-order = [ + "cross-compiler", + "tmp-tools", + "base", + "extended" +] + +[prod] +generate_iso = true diff --git a/bs/assests/cross-compiler.toml b/bs/assests/cross-compiler.toml new file mode 100644 index 0000000000000000000000000000000000000000..268c960664a257f3bcb15e88bd41e911d0ec6457 --- /dev/null +++ b/bs/assests/cross-compiler.toml @@ -0,0 +1,19 @@ +[env] +CALM = "/mnt/calm" +CALM_TGT = "x86_64-calm-linux-gnu" +LC_ALL = "POSIX" +PATH = "/bin:/mnt/calm/tools/bin" +CONFIG_SITE="/mnt/calm/usr/share/config.site" + +[stage] +jobs = 4 +src_dir = "/mnt/calm/usr/src" + +[packages] +plist = [ + "bs/cross/binutils", + "bs/cross/gcc", + "bs/cross/linux", + "bs/cross/glibc", + "bs/cross/libstdcpp", +] diff --git a/bs/assests/mkiso.toml b/bs/assests/mkiso.toml new file mode 100644 index 0000000000000000000000000000000000000000..13268e74421b898966c8ce4ec164798e00bac3db --- /dev/null +++ b/bs/assests/mkiso.toml @@ -0,0 +1,3 @@ +distro_name = "Calmira GNU/Linux-libre" +distro_version = "2.0a2" +label = "Calmira 2.0b1 GNU/Linux-libre install" diff --git a/bs/assests/tmp-tools.toml b/bs/assests/tmp-tools.toml new file mode 100644 index 0000000000000000000000000000000000000000..5784f7aa400b7755556f0358f4cb180d6ad267ed --- /dev/null +++ b/bs/assests/tmp-tools.toml @@ -0,0 +1,31 @@ +[env] +CALM = "/mnt/calm" +CALM_TGT = "x86_64-calm-linux-gnu" +LC_ALL = "POSIX" +PATH = "/bin:/mnt/calm/tools/bin" +CONFIG_SITE="/mnt/calm/usr/share/config.site" + +[stage] +jobs = 4 +src_dir = "/mnt/calm/usr/src" + +[packages] +plist = [ + "bs/tmp-tools/m4", + "bs/tmp-tools/ncurses", + "bs/tmp-tools/bash", + "bs/tmp-tools/coreutils", + "bs/tmp-tools/diffutils", + "bs/tmp-tools/file", + "bs/tmp-tools/findutils", + "bs/tmp-tools/gawk", + "bs/tmp-tools/grep", + "bs/tmp-tools/gzip", + "bs/tmp-tools/make", + "bs/tmp-tools/patch", + "bs/tmp-tools/sed", + "bs/tmp-tools/tar", + "bs/tmp-tools/xz", + "bs/tmp-tools/binutils", + "bs/tmp-tools/gcc" +] diff --git a/bs/src/config.rs b/bs/src/config.rs new file mode 100644 index 0000000000000000000000000000000000000000..b6bdb7bf7f1569fbf95abc529eda16921966fb04 --- /dev/null +++ b/bs/src/config.rs @@ -0,0 +1,85 @@ +//! Module for parsing build system configuration files +//! +//! ## Methods +//! - for serializing configs; +//! - for deserializing configs; +//! +//! ## Configs structure +//! +//! ### Build stage +//! +//! ```no-test +//! [env] +//! PATH = "/bin:/sbin:/usr/bin:/usr/sbin" +//! CALM = "/mnt/calm" +//! CALM_TGT = "x86_64_calm_linux_gnu" +//! +//! [stage] +//! jobs = 4 +//! src_dir = "/mnt/calm/usr/src" +//! +//! [packages] +//! plist = [ +//! "binutils-p1", +//! "gcc-p1", +//! "linux-api-headers", +//! "glibc", +//! ] +//! ``` +//! +//! ### `mkiso` +//! +//! ```no-test +//! distro_name = "Calmira GNU/Linux-libre" +//! distro_version = "v2.0a2b1" +//! label = "CALMIRA" +//! ``` +//! +//! ### `bs` (master config) +//! +//! ```no-test +//! build_order = ["cross-compiler", "toolchain", "base"] +//! generate_iso = true +//! ``` + +use serde::Deserialize; +use serde::Serialize; +use std::collections::HashMap; +use libcport::traits::TomlConfig; + +#[derive(Debug, Deserialize, Serialize)] +pub struct StageConfig { + env: HashMap, + stage: StageSection, + packages: PackagesSection, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct StageSection { + pub jobs: u8, + pub src_dir: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct PackagesSection { + pub plist: Vec, +} + +impl TomlConfig for StageConfig {} + +#[derive(Debug, Deserialize, Serialize)] +pub struct MkisoConfig { + pub distro_name: String, + pub distro_version: String, + pub label: String, +} + +impl TomlConfig for MkisoConfig {} + +#[derive(Debug, Deserialize, Serialize)] +pub struct BsConfig { + pub build_order: Vec, + pub generate_iso: bool, +} + +impl TomlConfig for BsConfig {} diff --git a/bs/src/lib.rs b/bs/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..9fb57891e59a59a94e9b61a19cdbfca129a3c14b --- /dev/null +++ b/bs/src/lib.rs @@ -0,0 +1,11 @@ +//! Calmira GNU/Linux-libre distribution automated build system +//! +//! ## Purpose +//! +//! 1. Automation of system building from source code. +//! 2. Generating bootable system images. + +pub mod config; +pub mod log; + +pub mod notifications; diff --git a/bs/src/log.rs b/bs/src/log.rs new file mode 100644 index 0000000000000000000000000000000000000000..ba2a2cedb5ad6781800936eec9a47478d7627573 --- /dev/null +++ b/bs/src/log.rs @@ -0,0 +1,65 @@ +use std::path::Path; + +use serde::Deserialize; +use serde::Serialize; +use libcport::errors::CportError; +use libcport::traits::TomlConfig; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct BuiltPortsList { + pub port: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct BuiltPort { + pub port_name: String, + pub port_version: String, + pub build_stage: String, + pub build_time: i64, +} + +impl TomlConfig for BuiltPortsList {} + +impl Default for BuiltPortsList { + fn default() -> Self { + Self { + port: Vec::new(), + } + } +} + +impl BuiltPortsList { + pub fn add>(pth: P, port: BuiltPort) -> Result<(), CportError> { + let mut ports_list = if pth.as_ref().exists() { + BuiltPortsList::parse(&pth)? + } else { + BuiltPortsList::default() + }; + + ports_list.port.push(port); + ports_list.write(pth)?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use libcport::consts::TEST_PNAME; + + const LOG_FILE: &str = "/tmp/ports.toml"; + + #[test] + fn add_ports_test() { + let port = BuiltPort { + port_name: TEST_PNAME.to_string(), + port_version: "v1.0".to_string(), + build_stage: "base".to_string(), + build_time: 1, + }; + for _ in 1..10 { + BuiltPortsList::add(LOG_FILE, port.clone()).unwrap(); + } + } +} diff --git a/bs/src/main.rs b/bs/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..417ab95145710399045b15acc2f072240b40e226 --- /dev/null +++ b/bs/src/main.rs @@ -0,0 +1,19 @@ +use bs::notifications::Notify; +use bs::notifications::NotifyType; + +fn main() { + let ports = [ + ("bs/cross/binutils", "2.41"), + ("bs/cross/gcc", "13.2.0"), + ("bs/cross/linux", "6.4.12"), + ("bs/cross/glibc", "2.38"), + ("bs/cross/libstdcpp", "13.2.0"), + ]; + Notify::new("bs/cross", None, None).show(NotifyType::NewStage).unwrap(); + for port in ports { + Notify::new("bs/cross", Some(port.0), Some(port.1)).show(NotifyType::NewPort).unwrap(); + } + Notify::new("bs/cross", Some("bs/cross/error-port"), Some("1.0")).show(NotifyType::ErrorPort).unwrap(); + std::thread::sleep(std::time::Duration::from_secs(1)); + Notify::new("bs/cross", None, None).show(NotifyType::EndStage).unwrap(); +} diff --git a/bs/src/notifications.rs b/bs/src/notifications.rs new file mode 100644 index 0000000000000000000000000000000000000000..9e8b1af593ca9980fb8cc7f9d1e108366842e0eb --- /dev/null +++ b/bs/src/notifications.rs @@ -0,0 +1,99 @@ +//! Desktop notifications +//! +//! ## Platform support +//! +//! Works correctly on the GNOME and KDE desctop environments + +use notify_rust::Hint; +use notify_rust::Urgency; +use notify_rust::Notification; + +const SUMMARY: &str = "Build system"; + +fn notify_new(icon: &str, app_name: T, body: T, urgency: Urgency) -> Notification { + Notification::new() + .summary(SUMMARY) + .icon(&icon.to_string()) + .appname(&app_name.to_string()) + .body(&body.to_string()) + .hint(Hint::Urgency(urgency)) + .clone() +} + +pub enum NotifyType { + NewStage, + EndStage, + ErrorStage, + NewPort, + ErrorPort, +} + +pub struct Notify<'a> { + port_name: Option<&'a str>, + port_version: Option<&'a str>, + build_stage: &'a str, +} + +impl<'a> Notify<'a> { + pub fn new(stage: &'a str, name: Option<&'a str>, ver: Option<&'a str>) -> Self { + Self { + port_name: name, + port_version: ver, + build_stage: stage, + } + } + + pub fn show(&self, ntype: NotifyType) -> Result<(), String> { + let mut notify = match ntype { + NotifyType::NewStage => notify_new( + "system-run", + format!("«{}» stage", self.build_stage), + format!("Move to the «{}» build stage", self.build_stage), + Urgency::Normal, + ), + NotifyType::EndStage => notify_new( + "system-run", + format!("«{}» stage", self.build_stage), + format!("Completion of the «{}» build stage", self.build_stage), + Urgency::Normal, + ), + NotifyType::ErrorStage => notify_new( + "dialog-error", + "Error during oper. of build stage".to_string(), + format!( + "An error occurred during the «{}» stage. Please open the \ + build console and check this out.", + self.build_stage, + ), + Urgency::Critical, + ), + NotifyType::NewPort => notify_new( + "package-x-generic", + format!("Building port «{}»", self.port_name.unwrap()), + format!( + "Port name: {} version {}", + self.port_name.unwrap(), + self.port_version.unwrap() + ), + Urgency::Normal, + ), + NotifyType::ErrorPort => notify_new( + "dialog-error", + format!("Build error («{}»)", self.port_name.unwrap()), + format!( + "Build error! Port name: {} version {}", + self.port_name.unwrap(), + self.port_version.unwrap() + ), + Urgency::Critical, + ), + }; + + notify + .timeout(0) + .show() + .map_err(|err| err.to_string())?; + + Ok(()) + } +} diff --git a/cport_cli/Cargo.toml b/cport_cli/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..2d5b83d65e8efdab6ebe0f5935f08b487ae48d27 --- /dev/null +++ b/cport_cli/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "cport_cli" +version = "2.0.0-a1" +edition = "2021" +authors = ["Michail Krasnov "] + +[[bin]] +name = "cport" +path = "src/bin/cport.rs" + +[dependencies] +libcport = { path = "../libcport" } + +anyhow.workspace = true +clap.workspace = true +colored.workspace = true +cursive.workspace = true +libmsg.workspace = true +serde.workspace = true +serde_json.workspace = true +i18n-embed.workspace = true +i18n-embed-fl.workspace = true +lazy_static.workspace = true +rust-embed.workspace = true diff --git a/cport_cli/README.md b/cport_cli/README.md new file mode 100644 index 0000000000000000000000000000000000000000..76c8a006cc2bb85576569b7d5fa3fc41f9465d61 --- /dev/null +++ b/cport_cli/README.md @@ -0,0 +1,205 @@ +# cport_cli + +![](assests/info.png) + +`cport_cli` (также называемый `cport`) — это текстовый пользовательский интерфейс для библиотеки `libcport`. Он использует функции этой библиотеки для установки, удаления и просмотра информации о портах. В будущем также будут предоставляться функции для обновления системы портов, обновления программного обеспечения и обслуживания системы. На данный момент ведётся активная разработка основных функций `cport_cli`. + +## TODO + +- [X] Установка программного обеспечения +- [X] Удаление программного обеспечения +- [ ] Поиск программного обеспечения по имени, описанию, сборщику +- [X] Просмотр информации о программном обеспечении +- [ ] Обновление программного обеспечения +- [ ] Обновление системы портов +- [ ] Обслуживание системы портов + - [ ] Проверка корректности портов + - [X] Проверка наличия зависимостей портов + - [X] Проверка наличия портов в СП + +## Использование cport + +### Установка порта + +![](assests/install.png) + +```bash +cport install [все порты, разделённые между собой пробелами] +``` + +Например: + +```bash +cport install base/acl general/which_orig xorg/wm/fluxbox editors/gvim +``` + +#### Изменение числа потоков + +`cport` по умолчанию собирает программное обеспечение в один поток, однако также поддерживает и многопоточную сборку портов. Для изменения количества потоков сборки (поддерживается до 255 потоков) используется ключ `--jobs`/`-j` с указанием количества потоков: + +```bash +cport install editors/gvim -j4 +``` + +Команда выше соберёт порт `editors/gvim` в 4 потока. + +> **Внимание:** `cport` не контролирует число потоков и автоматически не уменьшает их в случае необходимости. Не указывайте слижком большое число потоков для того, чтобы во время сборки порта не зависла система либо от нехватки ОЗУ, либо от чрезмерной нагрузки процессора. + +#### Изменение префикса установки + +*Префикс установки* — путь в файловой системе, куда будет установлены указанные порты. По умолчанию все порты устанавливаются в корень: `/`. Однако вы можете изменить префикс установки на тот, который нужен вам. Это обычно нужно, например, для создания бинарных пакетов из портов. Для изменения префикса установки используется ключ `--destination`/`-d` с указанием **абсолютного** пути: + +```bash +cport install editors/gvim -d $PWD/gvim +``` + +Команда выше установит порт `editors/gvim` в директорию `$PWD/gvim`. + +#### Тестирование портов + +Многие порты поддерживают *тестирование корректности сборки*. Подобное тестирование для некоторых портов часто или почти всегда заканчивается провалом, однако это не всегда может означать, что порт собран некорректно. Результаты тестирования могут быть полезны разработчикам как дистрибутива Calmira GNU/Linux-libre, так и разработчикам портируемого ПО. Для включения тестов используется ключ `--test`/`-t`. В случае отсутствия наборов тестов ничего вместо них исполняться не будет. + +```bash +cport install editors/gvim -t +``` + +#### Запросы продолжения + +Перед тем, как начать сборку порта, `cport` попросит пользователя подтвердить данную операцию: + +```bash +cport install editors/gvim +``` + +``` + Selected ports: ["editors/gvim"] + Build time: 19.7 SBU + Size: 166 Mb +:: Continue? [Y/n] +``` + +Для продолжения введите y или Y и нажмите Enter. `cport` начнёт скачивание исходного кода портов и их сборку. Однако подобные запросы продолжения будут крайне неудобны, если `cport`, например, используется в скриптах или каких-либо программах, не подразумевающих взаимодействие с ними пользователя, т.е. в тех скриптах или программах, работающих в *неинтерактивном* режиме. Для того, чтобы пропустить все запросы продолжения, т.е. **всегда** отвечать на них утвердительно, используйте ключ `--yes`/`-y`: + +```bash +cport install editors/gvim -y +``` + +``` + Selected ports: ["editors/gvim"] + Build time: 19.7 SBU + Size: 166 Mb +===> Install port editors/gvim +Download 'editors/gvim' 2s [====================] 10.63 MiB/10.63 MiB ETA: 0s +===> Extracting the package source code... + --> Extracting successful. +===> Switch to the working directory... + --> Switch successful. +===> Executing the 'prepare()' function... +configure: creating cache auto/config.cache +checking whether make sets $(MAKE)... yes +checking for gcc... gcc +checking whether the C compiler works... yes +checking for C compiler default output file name... a.out +checking for suffix of executables... + +... +``` + +`cport` вместо запроса у пользователя подтверждения операции сразу же приступит к сборке порта из исходного кода. + +### Удаление портов + +![](assests/remove.png) + +`cport` упрощает удаление установленных в систему портов. Для удаления требуется ввести команду: + +```bash +cport remove [список портов, разделённых между собой пробелами] +``` + +#### Разделение портов по приоритету + +Порты делятся на два приоритета: + +1. **Системный** - такие порты отвечают за корректную работу Calmira GNU/Linux-libre, а поэтому не могут быть удалены из системы средствами `cport`. +2. **Пользовательский** - с такими портами пользователи могут делать всё, что посчитают нужным: устанавливать, обновлять и удалять без каких-либо ограничений. + +Системные порты содержатся в категории `base/`. Их удаление из системы запрещено. + +#### Об удалении файлов + +`cport` способен удалять только *файлы* и *ссылки* на файлы. Каталоги, принадлежащие к удаляемому порту и указанные в файле `files.list`, ради обеспечения безопасности и работоспособности системы **игнорируются**. + +#### Удаление порта из альтернативного префикса установки + +По умолчанию все порты устанавливаются в `/`. Однако вы можете изменить префикс установки на тот, который требуется вам. По аналогии с установкой, для опции `remove` также есть ключ `--destination`/`-d`, позволяющий указать путь, по которому установлен порт: + +```bash +cport remove general/git editors/gvim -d /tmp/software +``` + +Эта команда говорит, что нам нужно удалить два порта (`general/git` и `editors/gvim`), оба из которых установлены в директорию `/tmp/software`. + +#### Подробный режим вывода + +При удалении `cport` не показывает удаляемые файлы. В том случае, если вы хотите видеть, *что именно* `cport` удаляет, используйте ключ `--verbose`/`-v`: + +```bash +cport remove general/git -v +``` + +#### Запросы продолжения + +Перед удалением порта `cport` также запрашивает продолжение операции. В том случае, если вы не хотите или вам не требуется отвечать на все из них, то можете использовать ключ `--yes`/`-y` для автоматического утвердительного ответа на все запросы продолжения. + +### Просмотр информации о программном обеспечении + +![](assests/info.png) + +Каждый порт содержит в себе конфигурационный файл `port.toml`, состоящий, как правило, из трёх секций: `package`, `deps` и `port`. Две последние опциональны, когда как секция `package` обязательна для всех портов: она содержит описания самых важных для `cport` данных: имя пакета, его версия, описание, занимаемое на диске место, приоритет и пр. + +Вручную искать этот конфигурационный файл и разбираться в наименовании и формате параметров из секции `package` может показаться неудобным, поэтому `cport` умеет форматировать сведения оттуда для чтения конкретным пользователем. + +Для получения сведений о программном обеспечении используется опция `info`: + +```bash +cport info [список портов, разделённых между собой пробелами] +``` + +#### Проверка наличия зависимостей + +К сожалению, в системе портов до сих пор отсутствуют многие зависимости к некоторым портам, в том числе и те зависимости, которые являются необходимыми. Для того, чтобы проверить наличие нужных портов перед сборкой используйте ключ `--check-deps`/`-c`. Он выведет список всех портов, которые присутствуют в СП, и всех портов, которые в СП отсутствуют. + +![](assests/check_deps.png) + +```bash +cport info base/wget -c +``` + +#### Альтернативный формат вывода: JSON + +Сведения, которые `cport` выводит в терминал, могут быть использованы в скриптах или каких-то программах. Однако выполнять их парсинг достаточно неудобно, а в некоторых случаях и невозможно, потому что формат вывода `cport` предназначен для чтения человеком, а не компьютером. Чтобы исправить это, вы можете выводить сведения в формате JSON. Их удобно парсить в скриптах и ином программном обеспечении и передавать по сети пользователям. + +Для включения вывода в формате JSON используется ключ `--json`/`-j`: + +```bash +cport info base/wget -j +``` + +Данный вывод сложно понять человеку. Для его парсинга из терминала Calmira GNU/Linux-libre можно использовать программу [`jq`](https://jqlang.github.io/jq/): + +```bash +cport info base/wget -j | jq +``` + +![](assests/info_json.png) + +Вы также можете комбинировать ключи `-c` и `-j`: + +```bash +# Выведет информацию о порте 'base/wget' и информацию о +# его зависимостях в формате JSON +cport info base/wget -cj +``` + +![](assests/info_json2.png) diff --git a/cport_cli/assests/check_deps.png b/cport_cli/assests/check_deps.png new file mode 100644 index 0000000000000000000000000000000000000000..315407c176fbffc132bdb41f5c272ea20deb3f9f Binary files /dev/null and b/cport_cli/assests/check_deps.png differ diff --git a/cport_cli/assests/info.png b/cport_cli/assests/info.png new file mode 100644 index 0000000000000000000000000000000000000000..c1a28fe2eb499e3d090cb0d1fb6490c524335f38 Binary files /dev/null and b/cport_cli/assests/info.png differ diff --git a/cport_cli/assests/info_json.png b/cport_cli/assests/info_json.png new file mode 100644 index 0000000000000000000000000000000000000000..d3e60cd1d5b1308e2595bb4953464029546a93c3 Binary files /dev/null and b/cport_cli/assests/info_json.png differ diff --git a/cport_cli/assests/info_json2.png b/cport_cli/assests/info_json2.png new file mode 100644 index 0000000000000000000000000000000000000000..5a06113a307bbc1ec590bb913479bceca6dbfc01 Binary files /dev/null and b/cport_cli/assests/info_json2.png differ diff --git a/cport_cli/assests/install.png b/cport_cli/assests/install.png new file mode 100644 index 0000000000000000000000000000000000000000..47439353e7173fab56ae3deca19f7431b2a9e06f Binary files /dev/null and b/cport_cli/assests/install.png differ diff --git a/cport_cli/assests/remove.png b/cport_cli/assests/remove.png new file mode 100644 index 0000000000000000000000000000000000000000..6e29f1b80dd3f6530a730b1b210cd5679233289c Binary files /dev/null and b/cport_cli/assests/remove.png differ diff --git a/cport_cli/i18n.toml b/cport_cli/i18n.toml new file mode 100644 index 0000000000000000000000000000000000000000..76f7c3103819275f35cbbeb80a6fa6791e3fe9b3 --- /dev/null +++ b/cport_cli/i18n.toml @@ -0,0 +1,4 @@ +fallback_language = "en" + +[fluent] +assets_dir = "i18n" diff --git a/cport_cli/i18n/en/cport_cli.ftl b/cport_cli/i18n/en/cport_cli.ftl new file mode 100644 index 0000000000000000000000000000000000000000..07df557a30ba253c6c6d21e1bcd20e29c4116793 --- /dev/null +++ b/cport_cli/i18n/en/cport_cli.ftl @@ -0,0 +1,104 @@ +# cport_cli +not-implemented = + This functionality is not yet implemented (cport version + : {$pkg_version}). If you see this message, it means that developers know about the lack of + necessary functions and will make them soon ;) +threads-error = The number of threads cannot be equal to 0! +exists = exists +not-exists = doesn't exists +db-gen = Generating a database of installed software... +dest-path = dest path +db-gen-success = Generation successful! + +# --help +description = cport is a program for managing software in Calmira GNU/Linux-libre +no-color = Disables colored `cport` output +install = Install ports to the system +install-ports = List of ports to build and install +install-threads = Number of build threads (1..255) +install-dir = Directory where the port will be installed +help-yes = Answer all questions affirmatively +install-tests = Whether to execute tests after the port is built +remove = Remove installed ports from the system +remove-ports = List of ports to remove +remove-dir = Directory where the port is installed +info = Displays basic information about the software +info-ports = List of ports +info-check = + Check the status of port dependencies (installed on the + system, not installed on the system, missing ports) +list = Displays a list of ports +list-formatted = Formatted output, including version and a short description of the port +update = Update ports system +update-changes = Display the list of changes and exit +upgrade = Upgrade ports +upgrade-ports = List of ports to upgrade +check = Checks for the presence of a port in the port system (for scripts) +check-port = Port name +check-exists = Checking if a port is available in the port system +check-installed = Checking the port for available in the database +check-not-installed = Checking the port for missing in the database +service = Service functions usually performed after `cport` is installed on the system +service-gen-db = Creates a new database of the installed software +service-destination = Path to the root directory where all the specified actions will be executed + +# info +missing-metadata = + missing in the metadata. Update metadata + for correct port system information +missing-ports = + missing in the PORTS. Update metadata + for correct port system information +missing-everywhere = missing everywhere +port-name = port name +dep-type = dependency type +in-metadata = in metadata +in-ports = in ports system +summary = summary +deps-not-found = dependencies not found +port-info = Information about this port: +port-info-name = name (port) +port-info-package = name (package) +port-info-version = version +port-info-priority = priority +port-info-usage = usage +port-info-build-time = build time +port-info-description = description +port-info-deps-status = Information about dependencies status: +selected-ports = Selected ports +build-time = Build time +build-time-all = Build time [all] +size = Size +size-all = Size [all] +db-err-port-present = Ambiguous database entry: {$port_len} records of the requested port are present +port-installed = installed +port-not-installed = not installed + +# install +about = About +port-message = + Port {$port_name} contains a message. Be sure to + read it before continuing. +read-message-failed = Failed to read 'message' file! +error = Error +running-build = Running build instructions of port {$port_name} +wait-for-build = + Building this port may take some time. + No messages will be displayed while the + build is in progress. +building-complete = Building port {$port_name} complete! +continue-building = Continue building? +program-exit = There was an exit from the program +install-port = Install port {$port_name}... +add-to-db = Add port {$port_name} to the database... +complete = Complete! + +# list +get-description-failed = could not get port description! + +# remove +invalid-action = The user performed an invalid action. +process-aborted = The `cport` process will be aborted! +sys-port-delete-fail = System ports cannot be deleted +continue-removing = Continue removing? +remove-port = Remove port {$port_name} \ No newline at end of file diff --git a/cport_cli/i18n/ru/cport_cli.ftl b/cport_cli/i18n/ru/cport_cli.ftl new file mode 100644 index 0000000000000000000000000000000000000000..7687c82b3a78767eaad4c3cf48f2800b1f0555d5 --- /dev/null +++ b/cport_cli/i18n/ru/cport_cli.ftl @@ -0,0 +1,103 @@ +# cport_cli +not-implemented = + Данный функционал ещё не реализован (версия cport: {$pkg_version}). Если вы + видете это сообщение, это значит, что разработчики знают об отсутствии + необходимых функций и сделают их в ближайшее время ;) +threads-error = Номер потоков не должен равняться 0! +exists = существует +not-exists = не существует +db-gen = Генерация БД установленного ПО... +dest-path = путь установки +db-gen-success = Генерация прошла успешно! + +# --help +description = cport — программа для управления ПО в Calmira GNU/Linux-libre +no-color = Отключает цветной вывод `cport` +install = Установить порты в систему +install-ports = Список портов для сборки и установки +install-threads = Число потоков сборки (1..255) +install-dir = Директория, куда будет установлен порт +help-yes = Отвечать на все вопросы положительно +install-tests = Выполнять ли тесты после сборки порта +remove = Удалить установленные порты из системы +remove-ports = Список портов для удаления +remove-dir = Директория, в которой установлен порт +info = Отобразить основную информацию о ПО +info-ports = Список портов +info-check = + Проверить статус зависимостей порта (установлены в систему, + не установлены в систему или отсутствуют) +list = Отобразить список портов +list-formatted = Форматированный вывод, включающий версию порта и его краткое описание +update = Обновить систему портов +update-changes = Отобразить список изменений и выйти +upgrade = Обновить порты +upgrade-ports = Список портов для обновления +check = Проверить наличие порта в системе портов (для скриптов) +check-port = Имя порта +check-exists = Проверить наличие порта в системе портов +check-installed = Проверить наличие порта в базе данных +check-not-installed = Проверить отсутствие порта в базе данных +service = Сервисные функции, обычно выполняемые после установки `cport` в систему +service-gen-db = Создать новую базу данных установленного ПО +service-destination = Путь к корневому каталогу, где будут выполняться все указанные действия + +# info +missing-metadata = + отсутствует в метаданных. Обновите метаданные + для получения корректной информации о системе портов +missing-ports = + отсутствует в СИСТЕМЕ ПОРТОВ. Обновите метаданные + для получения корректной информации о системе портов +missing-everywhere = отсутствует везде +port-name = имя порта +dep-type = тип зависимости +in-metadata = в метаданных +in-ports = в портах +summary = итого +deps-not-found = зависимости отсутствуют +port-info = Информация о данном порте: +port-info-name = имя (порта) +port-info-package = имя (пакета) +port-info-version = версия +port-info-priority = приоритет +port-info-usage = размер +port-info-build-time = время сборки +port-info-description = описание +port-info-deps-status = Информация о статусе зависимостей: +selected-ports = Выбранные порты +build-time = Время сборки +build-time-all = Время сборки [всё] +size = Размер +size-all = Размер [весь] +db-err-port-present = Неоднозначная запись в базе данных: {$port_len} записи(ей) о запрашиваемом порте присутствуют +port-installed = установлен +port-not-installed = не установлен + +# install +about = О `cport` +port-message = + Порт {$port_name} содержит сообщение. Обязательно + прочитайте его перед продолжением. +read-message-failed = Ошибка чтения файла 'message'! +error = Ошибка +running-build = Исполнение сборочных инструкций порта {$port_name} +wait-for-build = + Сборка данного порта может занять некоторое время. + Никакие сообщения в это время не будут отображены. +building-complete = Сборка порта {$port_name} завершена! +continue-building = Продолжить сборку? +program-exit = Произошёл выход из программы +install-port = Установка порта {$port_name}... +add-to-db = Добавление порта {$port_name} в базу данных... +complete = Готово! + +# list +get-description-failed = не удалось получить описание порта! + +# remove +invalid-action = Пользователь выполнил недопустимое действие. +process-aborted = Процесс `cport` был завершён +sys-port-delete-fail = Системные порты не могут быть удалены +continue-removing = Продолжить удаление? +remove-port = Удаление порта {$port_name} diff --git a/cport_cli/i18n/sv/cport_cli.ftl b/cport_cli/i18n/sv/cport_cli.ftl new file mode 100644 index 0000000000000000000000000000000000000000..682183f9a63f9bbe9a657be6c1943ce999e774a0 --- /dev/null +++ b/cport_cli/i18n/sv/cport_cli.ftl @@ -0,0 +1,105 @@ +# cport_cli +not-implemented = + Detta funktionalitet är inte implementerat (cport version + : {$pkg_version}). Om du ser detta meddelande, betyder det att + utvcklarna vet om att det saknas funktioner och vill lägga till dem snart. ;) +threads-error = Antal trådar får inte vara 0! +exists = existerar +not-exists = finns inte +db-gen = Genererar databasen med installerat programvaran... +dest-path = destinationsväg +db-gen-success = Generation lyckades! + +# --help +description = cport är ett program för att hantera programvara i Calmira GNU/Linux-libre +no-color = Inaktivera färgad `cport`-utskrift +install = Installera portar i systemet +install-ports = Lista portar till bygge och installation +install-threads = Antal byggtrådar (1..255) +install-dir = Mapp vart port ska installeras +help-yes = Svara jakande på alla frågor +install-tests = Om tester ska utföras efter att porten har byggts +remove = Radera installerade portar från systemet +remove-ports = Lista över portar att ta bort +remove-dir = Mapp där porten är installerad +info = Visar grundläggande information om programvaran +info-ports = Lista över portar +info-check = + Kontrollera statusen för portberoenden (installerad i + systemet, inte installerat i systemet, saknade portar) +list = Visar en lista över portar +list-formatted = Formaterad utdata, inklusive version och en kort beskrivning av porten +update = Uppdatera portsystem +update-changes = Visa listan med ändringar och avsluta +upgrade = Uppgradera portar +upgrade-ports = Lista över portar att uppgradera +check = Kontrollera om det finns en port i portsystemet (för skript) +check-port = Port namn +check-exists = Kontrollera om en port är tillgänglig i portsystemet +check-installed = Kontrollerar porten för tillgänglig i databasen +check-not-installed = Kontrollerar att porten saknas i databasen +service = Servicefunktioner utförs vanligtvis efter att `cport` har installerats i systemet +service-gen-db = Skapar en ny databas med den installerade programvaran +service-destination = Sökväg till rotkatalogen där alla angivna åtgärder kommer att utföras + +# info +missing-metadata = + saknas i metadata. Uppdatera metadata + för korrekt portsysteminformation +missing-ports = + saknas i PORTS. Uppdatera metadata + för korrekt portsysteminformation +missing-everywhere = saknas överallt +port-name = port namn +dep-type = beroendetyp +in-metadata = i metadata +in-ports = i portsystem +summary = sammanfattning +deps-not-found = beroenden hittades inte +port-info = Information om denna port: +port-info-name = namn (port) +port-info-package = namn (package) +port-info-version = version +port-info-priority = prioritet +port-info-usage = användning +port-info-build-time = byggtid +port-info-description = beskrivning +port-info-deps-status = Information om beroendestatus: +selected-ports = Utvalda portar +build-time = Bygg tid +build-time-all = Bygg tid [total] +size = Storlek +size-all = Storlek [total] +db-err-port-present = Tvetydig databaspost: {$port_len} poster för den begärda porten finns +port-installed = installerat +port-not-installed = inte installerat + + +# install +about = Om +port-message = + Port {$port_name} innehåller ett meddelande. Var säker på att + läs den innan du fortsätter. +read-message-failed = Det gick inte att läsa 'message'-filen! +error = Error +running-build = Kör bygginstruktioner för port {$port_name} +wait-for-build = + Det kan ta lite tid att bygga den här hamnen. + Inga meddelanden kommer att visas medan + bygget pågår. +building-complete = Byggandet av port {$port_name} är klar! +continue-building = Fortsätt bygga? +program-exit = Det fanns en utgång från programmet +install-port = Installerar port {$port_name}... +add-to-db = Lägg till port {$port_name} till databasen... +complete = Komplett! + +# list +get-description-failed = kunde inte få portbeskrivning! + +# remove +invalid-action = Användaren utförde en ogiltig åtgärd. +process-aborted = `cport`-processen kommer att avbrytas! +sys-port-delete-fail = Systemportar kan inte tas bort +continue-removing = Vill du fortsätta att ta bort? +remove-port = Ta bort porten {$port_name} diff --git a/cport_cli/src/bin/cport.rs b/cport_cli/src/bin/cport.rs new file mode 100644 index 0000000000000000000000000000000000000000..b320e4ef0b4737e1b67c65f6675aea3e1313e277 --- /dev/null +++ b/cport_cli/src/bin/cport.rs @@ -0,0 +1,167 @@ +use colored::Colorize; +use cport_cli::cli::*; +use cport_cli::fl; +use cport_cli::info; +use cport_cli::install; +use cport_cli::list; +use cport_cli::remove; +use cport_cli::service; +use libcport::port::Port; +use libmsg::Message; +use std::process; + +fn unimplemented_msg() { + let msg = fl!("not-implemented", pkg_version = env!("CARGO_PKG_VERSION")); + println!("{}", msg.bold().red()); + process::exit(1); +} + +fn main() { + let cli = Cli::parse(); + let mut msg = Message::default(); + + match std::env::var("CPORT_COLOR") { + Ok(val) => { + if &val == "NO" || &val == "no" { + libmsg::set_colors(false); + } else { + libmsg::set_colors(!cli.enable_no_color); + } + } + Err(_) => libmsg::set_colors(!cli.enable_no_color), + } + + match &cli.command { + Command::Install { + ports, + jobs, + destination, + test, + yes, + verbose, + } => { + msg.set_oper("install"); + if *jobs == 0 { + msg.error(fl!("threads-error"), true); + } + + if let Err(why) = install::install(ports, *jobs, destination, *test, *yes, *verbose) { + msg.error(why, true); + } + } + Command::Remove { + ports, + destination, + verbose, + yes, + } => { + msg.set_oper("remove"); + if let Err(why) = remove::remove(ports, destination, *verbose, *yes) { + msg.error(why, true); + } + } + Command::List { + formatted, + only_normal, + without_versions, + json, + } => { + msg.set_oper("list"); + match list::List::new() { + Ok(list) => { + if *formatted { + if *only_normal { + list.print_normal_formatted(!*without_versions); + } + + if let Err(why) = list.print_all_formatted() { + msg.error(why, true); + } + } else if *json { + if let Err(why) = list.print_all_json() { + msg.error(why, true); + } + } else if *only_normal { + list.print_normal_raw(!*without_versions); + } else { + list.print_all_raw(!*without_versions); + } + } + Err(why) => msg.error(why, true), + } + } + Command::Info { + ports, + check_deps, + json, + } => { + msg.set_oper("info"); + for port in ports { + if *json { + if let Err(why) = info::print_info_json(port, *check_deps) { + msg.error(why, true); + } + } else if let Err(why) = info::print_info_formatted(port, *check_deps) { + msg.error(why, true); + } + } + } + Command::Check { + port, + is_exists, + is_installed, + is_not_installed, + } => { + if *is_exists { + if libcport::port::Port::try_new(port).is_ok() { + println!("{port}: {}", fl!("exists")); + process::exit(0); + } else { + println!("{port}: {}", fl!("not-exists")); + process::exit(1); + } + } else if *is_installed { + msg.set_oper("check"); + match Port::try_new(port) { + Ok(p) => match info::is_installed(&p) { + Ok(code) => process::exit(code), + Err(why) => msg.error(why, true), + }, + Err(why) => msg.error(why, true), + } + } else if *is_not_installed { + match Port::try_new(port) { + Ok(p) => match info::is_installed(&p) { + Ok(code) => { + if code == 0 { + process::exit(1); + } else { + process::exit(0); + } + } + Err(why) => msg.error(why, true), + }, + Err(why) => msg.error(why, true), + } + } + } + Command::Service { + generate_database, + destination, + .. + } => { + msg.set_oper("service"); + if *generate_database { + msg.msg(fl!("db-gen")); + msg.set_indent(2); + msg.del_oper(); + msg.info(format!("{}: {}", fl!("dest-path").dimmed(), destination)); + if let Err(why) = service::generate_database(destination) { + msg.error(why, true); + } + msg.ok(fl!("db-gen-success")); + } + } + _ => unimplemented_msg(), + } +} diff --git a/cport_cli/src/cli.rs b/cport_cli/src/cli.rs new file mode 100644 index 0000000000000000000000000000000000000000..b5c9e22380e47e9d24550ed30175189e4fb22c45 --- /dev/null +++ b/cport_cli/src/cli.rs @@ -0,0 +1,116 @@ +pub use clap::Parser; +use clap::Subcommand; + +use crate::fl; + +#[derive(Parser)] +#[command(author, version, about = fl!("description"), long_about = None)] +pub struct Cli { + #[command(subcommand)] + pub command: Command, + + #[arg(short = 'C', long, help = fl!("no-color"))] + pub enable_no_color: bool, +} + +#[derive(Subcommand)] +pub enum Command { + #[command(about = fl!("install"))] + Install { + #[arg(help = fl!("install-ports"))] + ports: Vec, + + #[arg(short, long, default_value_t = 1, help = fl!("install-threads"))] + jobs: u8, + + #[arg(short, long, default_value_t = String::from("/"), help = fl!("install-dir"))] + destination: String, + + #[arg(short, long, help = fl!("help-yes"))] + yes: bool, + + #[arg(short, long, help = fl!("install-tests"))] + test: bool, + + #[arg(short, long)] + verbose: bool, + }, + + #[command(about = fl!("remove"))] + Remove { + #[arg(help = fl!("remove-ports"))] + ports: Vec, + + #[arg(short, long, default_value_t = String::from("/"), help = fl!("remove-dir"))] + destination: String, + + #[arg(short, long)] + verbose: bool, + + #[arg(short, long, help = fl!("help-yes"))] + yes: bool, + }, + + #[command(about = fl!("info"))] + Info { + #[arg(help = fl!("info-ports"))] + ports: Vec, + + #[arg(short, long, help = fl!("info-check"))] + check_deps: bool, + + #[arg(short, long)] + json: bool, + }, + + #[command(about = fl!("list"))] + List { + #[arg(short, long, help = fl!("list-formatted"))] + formatted: bool, + + #[arg(short = 'n', long)] + only_normal: bool, + + #[arg(short = 'v', long)] + without_versions: bool, + + #[arg(short, long)] + json: bool, + }, + + #[command(about = fl!("update"))] + Update { + #[arg(short, long, help = fl!("update-changes"))] + show_changelog_only: bool, + }, + + #[command(about = fl!("upgrade"))] + Upgrade { + #[arg(help = fl!("upgrade-ports"))] + ports: Vec, + }, + + #[command(about = fl!("check"))] + Check { + #[arg(help = fl!("check-port"))] + port: String, + + #[arg(short = 'e', long, help = fl!("check-exists"))] + is_exists: bool, + + #[arg(short = 'i', long, help = fl!("check-installed"))] + is_installed: bool, + + #[arg(short = 'I', long, help = fl!("check-not-installed"))] + is_not_installed: bool, + }, + + #[command(about = fl!("service"))] + Service { + #[arg(short = 'd', long, help = fl!("service-gen-db"))] + generate_database: bool, + + #[arg(short = 'D', long, help = fl!("service-destination"))] + destination: String, + }, +} diff --git a/cport_cli/src/i18n.rs b/cport_cli/src/i18n.rs new file mode 100644 index 0000000000000000000000000000000000000000..52ca5f8445e75f2013c0641b710f701141ae20db --- /dev/null +++ b/cport_cli/src/i18n.rs @@ -0,0 +1,32 @@ +use i18n_embed::{ + fluent::{fluent_language_loader, FluentLanguageLoader}, + DesktopLanguageRequester, +}; +use lazy_static::lazy_static; +use rust_embed::RustEmbed; + +#[derive(RustEmbed)] +#[folder = "/usr/share/locale"] +struct Localizations; + +fn read() -> FluentLanguageLoader { + let loader: FluentLanguageLoader = fluent_language_loader!(); + let req_langs = DesktopLanguageRequester::requested_languages(); + i18n_embed::select(&loader, &Localizations, &req_langs).unwrap(); + loader +} + +lazy_static! { + pub static ref LANG_LOADER: FluentLanguageLoader = read(); +} + +#[macro_export] +macro_rules! fl { + ($message_id:literal) => {{ + i18n_embed_fl::fl!($crate::i18n::LANG_LOADER, $message_id) + }}; + + ($message_id:literal, $($args:expr),*) => {{ + i18n_embed_fl::fl!($crate::i18n::LANG_LOADER, $message_id, $($args), *) + }}; +} diff --git a/cport_cli/src/info.rs b/cport_cli/src/info.rs new file mode 100644 index 0000000000000000000000000000000000000000..ea3b1634bba5bf41b78d04ec670f6a845c702c60 --- /dev/null +++ b/cport_cli/src/info.rs @@ -0,0 +1,281 @@ +use colored::Colorize; +use libcport::consts::DATABASE; +use serde::Deserialize; +use serde::Serialize; +use std::fmt::Display; + +use libcport::consts::METADATA_FILE; +use libcport::db::Database; +use libcport::errors::CportError; +use libcport::metadata::Metadata; +use libcport::port::PackageConfigSection; +use libcport::port::Port; +use libcport::port::PortConfig; +use libcport::traits::TomlConfig; +use libmsg::Message; + +use crate::fl; + +#[derive(PartialEq)] +pub enum Operation { + Install, + Remove, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct DependencyStatus { + /// Name of port (e.g. `"base/acl"`) + pub port_name: String, + + /// Port dependency type + /// + /// ## Types: + /// 1. `required` + /// 2. `recommend` + /// 3. `optional` + pub dependency_type: String, + + /// Does the port exist in the metadata + pub in_metadata: bool, + + /// Does the port exist on the file system + pub in_filesystem: bool, +} + +// Костыль для того, чтобы нормально сгенерировать +// JSON на основе необходимых данных +#[derive(Debug, Deserialize, Serialize)] +pub struct CheckDepsJson { + pub package: PackageConfigSection, + pub deps: Vec, +} + +impl DependencyStatus { + pub fn new(port_name: &String, dep_type: &str, metadata: &Metadata) -> Self { + let in_filesystem = Port::try_new(port_name).is_ok(); + let in_metadata = metadata.search_port(port_name).is_some(); + + Self { + port_name: port_name.to_string(), + dependency_type: dep_type.to_string(), + in_filesystem, + in_metadata, + } + } +} + +impl Display for DependencyStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let in_metadata = match self.in_metadata { + true => "yes".green(), + false => "NO".red(), + }; + let in_ports = match self.in_filesystem { + true => "yes".green(), + false => "NO".red(), + }; + + let summary = if self.in_metadata && self.in_filesystem { + "ok".green() + } else if !self.in_metadata && self.in_filesystem { + fl!("missing-metadata").yellow() + } else if self.in_metadata && !self.in_filesystem { + fl!("missing-ports").bold().red() + } else { + fl!("missing-everywhere").bold().red() + }; + + let content = format!( + "{:>15}: {}\n{:>15}: {}\n{:>15}: {}\n{:>15}: {}\n{:>15}: {}", + // "{:>15}: {}\n{:>15}: {}\n{:>15}: {}", + fl!("port-name").bold(), + &self.port_name.underline(), + fl!("dep-type").bold(), + &self.dependency_type, + fl!("in-metadata").bold(), + in_metadata, + fl!("in-ports").bold(), + in_ports, + fl!("summary").bold(), + summary, + ); + write!(f, "{}", content) + } +} + +pub fn check_dependencies(port: &Port) -> Result, CportError> { + let pconf = PortConfig::from_port(port)?; + let metadata = Metadata::parse(METADATA_FILE)?; + + if pconf.deps.is_none() { + return Err(CportError::PortDepsNotFound(port.name.clone())); + } + let deps = &pconf.deps.unwrap(); + + /* + * На случай, если в `port.toml` указана пуская секция `deps` + */ + if deps.required.is_none() && deps.recommend.is_none() && deps.optional.is_none() { + return Err(CportError::PortDepsNotFound(port.name.clone())); + } + + let mut dependencies = Vec::new(); + let dep_types = [ + ("required", &deps.required), + ("recommend", &deps.recommend), + ("optional", &deps.optional), + ]; + + for dep_t in dep_types { + if let Some(deps) = dep_t.1 { + for dep in deps { + dependencies.push(DependencyStatus::new(dep, dep_t.0, &metadata)); + } + } + } + + Ok(dependencies) +} + +pub fn print_info_formatted(port: &str, check_deps: bool) -> Result<(), CportError> { + let msg = Message::default(); + let port = Port::try_new(port)?; + let pconf = PortConfig::from_port(&port)?; + let pkg_s = &pconf.package; + + msg.msg(fl!("port-info").bold()); + println!("{:>15}: {}", fl!("port-info-name").bold(), &port.name); + println!("{:>15}: {}", fl!("port-info-package").bold(), &pkg_s.name); + println!( + "{:>15}: {}", + fl!("port-info-version").bold(), + &pkg_s.version + ); + println!( + "{:>15}: {}", + fl!("port-info-priority").bold(), + &pkg_s.priority + ); + println!("{:>15}: {} MiB", fl!("port-info-usage").bold(), pkg_s.usage); + println!( + "{:>15}: {}", + fl!("port-info-build-time").bold(), + pkg_s.build_time + ); + println!( + "{:>15}: {}\n", + fl!("port-info-description").bold(), + &pkg_s.description + ); + + if check_deps { + match check_dependencies(&port) { + Ok(deps) => { + msg.msg2(fl!("port-info-deps-status").bold()); + for dep in deps { + println!("{dep}\n"); + } + println!("———————————————————————————————————————\n"); + }, + Err(why) => match why { + CportError::PortDepsNotFound(_) => { + msg.warn(&why.to_string()); + return Ok(()); + }, + _ => return Err(why), + } + } + } + + Ok(()) +} + +pub fn print_info_json(port: &str, check_deps: bool) -> Result<(), CportError> { + let port = Port::try_new(port)?; + let pconf = PortConfig::from_port(&port)?; + let pkg_s = &pconf.package; + + if check_deps { + let deps_lst = CheckDepsJson { + package: pkg_s.clone(), + deps: check_dependencies(&port)?, + }; + let deps_string = serde_json::to_string(&deps_lst) + .map_err(|err| CportError::Deserialize(err.to_string()))?; + println!("{deps_string}"); + } else { + let pkg_string = + serde_json::to_string(pkg_s).map_err(|err| CportError::Deserialize(err.to_string()))?; + println!("{pkg_string}"); + } + + Ok(()) +} + +pub fn print_summary(ports: &Vec, op: Operation) -> Result<(), CportError> { + let mut port_usage = 0.; + let mut build_time = 0.; + let mut port_names = vec![]; + + for port in ports { + port_names.push(&port.name); + let pconf = PortConfig::from_port(port)?; + port_usage += pconf.package.usage; + build_time += pconf.package.build_time; + } + + println!("{:>16}: {:?}", fl!("selected-ports").bold(), port_names); + + if op == Operation::Install { + println!( + "{:>16}: {} SBU", + if ports.len() == 1 { + fl!("build-time").bold() + } else { + fl!("build-time-all").bold() + }, + // Всё, что собирается меньше, чем за 3 SBU, считаем быстрым + // Время сборки = 3 SBU - средним + // Время сборки > 3 SBU - долгим + if build_time < 3. { + build_time.to_string().green() + } else if build_time == 3. { + build_time.to_string().yellow() + } else { + build_time.to_string().red() + }, + ); + } + + println!( + "{:>16}: {} Mb", + if ports.len() == 1 { + fl!("size").bold() + } else { + fl!("size-all").bold() + }, + port_usage + ); + + Ok(()) +} + +pub fn is_installed(port: &Port) -> Result { + let db = Database::open(DATABASE)?; + let prt = db.get_port(port)?; + + if prt.len() > 1 { + return Err(CportError::DatabaseError(fl!( + "db-err-port-present", + port_len = prt.len() + ))); + } + + Ok(if prt.len() == 1 { + println!("{}: {}", &port.name, fl!("port-installed").green()); + 0 + } else { + println!("{}: {}", &port.name, fl!("port-not-installed").red()); + 1 + }) +} diff --git a/cport_cli/src/install.rs b/cport_cli/src/install.rs new file mode 100644 index 0000000000000000000000000000000000000000..e00f2b7c3ca10745521fbaf69c68187b42229755 --- /dev/null +++ b/cport_cli/src/install.rs @@ -0,0 +1,209 @@ +use colored::Colorize; +use std::fs::read_to_string; +use std::path::Path; + +use libcport::consts::DATABASE; +use libcport::db::Database; +use libcport::db::Installed; +use libcport::errors::CportError; +use libcport::install::BuildResult; +use libcport::install::Install; +use libcport::port::Port; +use libcport::port::PortConfig; +use libcport::traits::TomlConfig; + +use libmsg::tui::about::AboutWindowBuilder; +use libmsg::tui::about::Copyright; +use libmsg::tui::error::ErrorBuilder; +use libmsg::tui::init; +use libmsg::Message; + +use cursive::event::Key; +use cursive::traits::Scrollable; +use cursive::views::Dialog; +use cursive::views::LinearLayout; +use cursive::views::Panel; +use cursive::views::TextView; + +use crate::fl; +use crate::info::print_summary; +use crate::info::Operation; + +fn view_message(port: &Port) -> Result<(), CportError> { + let msg_pth = Path::new(&port.port_pth).join("message"); + if !msg_pth.is_file() { + return Ok(()); + } + + let mut scr = init(); + + scr.add_global_callback(Key::Enter, |s| s.quit()); + scr.add_global_callback(Key::F1, |s| { + let about_win = Dialog::around( + AboutWindowBuilder::new("cport", env!("CARGO_PKG_VERSION")) + .set_description(env!("CARGO_PKG_DESCRIPTION")) + .set_site("https://gitlab.com/calmiralinux/cabs/cport") + .add_copyright(Copyright::new( + "2022, 2023", + "Michail Krasnov", + "linuxoid85@gmail.com", + )) + .build() + .window(), + ) + .title(fl!("about")) + .button("OK", |s| { + s.pop_layer(); + }); + + s.add_layer(about_win); + }); + + /* + * Допустимые переменные: + * - `$PORT_NAME` - имя порта + * - `$NAME` - имя пакета + * - `$VERSION` - версия пакета + * - `$PORT_DIR` - путь до порта в файловой системе + */ + match read_to_string(msg_pth) { + Ok(t) => { + let pconf = PortConfig::from_port(port)?; + let msg_txt = t + .replace("$PORT_NAME", &port.name) + .replace("$PORT_DIR", &port.port_pth) + .replace("$NAME", &pconf.package.name) + .replace("$VERSION", &pconf.package.version) + .replace('\t', " "); + + let msg_view = Panel::new(TextView::new(msg_txt).scrollable()); + + let header_txt = TextView::new(fl!("port-message", port_name = port.name.clone())); + + let win_layout = LinearLayout::vertical().child(header_txt).child(msg_view); + let win = Dialog::around(win_layout) + .title(format!( + "{}-{}", + env!("CARGO_PKG_NAME"), + env!("CARGO_PKG_VERSION") + )) + .button("OK", |s| s.quit()); + + scr.add_layer(win); + } + Err(why) => { + let mut err = ErrorBuilder::new(&fl!("read-message-failed")) + .set_traceback_text(&format!("{why}")) + .build(); + scr.add_layer( + Dialog::around(err.window()) + .title(&fl!("error")) + .button("OK", |s| s.quit()), + ); + } + }; + + scr.run(); + + Ok(()) +} + +fn install_port( + port: &Port, + jobs: u8, + dest: &str, + test: bool, + verbose: bool, +) -> Result { + let install = Install::new(port, jobs, "/usr/src", dest, test, verbose); + install + .download() + .map_err(|err| CportError::DownloadFile(err.to_string()))?; + + let mut msg = Message::default(); + msg.msg(fl!( + "running-build", + port_name = port.name.bold().to_string() + )); + + if !verbose { + msg.set_indent(4); + msg.msg2(fl!("wait-for-build")); + msg.set_indent(0); + } + + let run = install + .run_inst_script() + .map_err(|err| CportError::Run(err.to_string()))?; + + if run.code != 0 { + return Err(CportError::RunCode(run.code)); + } + + msg.ok(fl!( + "building-complete", + port_name = port.name.bold().to_string() + )); + + view_message(port)?; + + Ok(run) +} + +pub fn install( + ports: &[String], + jobs: u8, + dest: &str, + test: bool, + yes: bool, + verbose: bool, +) -> Result { + let mut inst_ports = vec![]; + for port in ports { + // Работа `install()` завершится, если какого-то порта нет + // или экземпляр `libcport::port::Port` по иным причинам + // невозможно создать. В предыдущем варианте функция + // всё равно продолжала свою работу, не уведомляя человека + // о наличии ошибок. + inst_ports.push(Port::try_new(port)?); + } + + let mut msg = Message::default(); + print_summary(&inst_ports, Operation::Install)?; + if !yes + && !msg + .dialog(fl!("continue-building"), false) + .map_err(CportError::ReadFile)? + { + return Err(CportError::Run(fl!("program-exit"))); + } + + for port in &inst_ports { + msg.msg(fl!( + "install-port", + port_name = port.name.bold().to_string() + )); + + let mut conn = Database::open(DATABASE)?; + let pconf = PortConfig::from_port(port)?; + + let run = install_port(port, jobs, dest, test, verbose)?; + + let inst_port = Installed { + name: port.name.to_string(), + version: pconf.package.version.clone(), + usage: pconf.package.usage, + build_time: run.time / 3.15, + }; + + msg.msg(fl!("add-to-db", port_name = port.name.bold().to_string())); + conn.add_port(&inst_port)?; + conn.conn + .close() + .map_err(|err| CportError::DatabaseError(err.1.to_string()))?; + msg.set_indent(4); + msg.msg2(fl!("complete")); + } + + Ok(0) +} diff --git a/cport_cli/src/lib.rs b/cport_cli/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..7d5d63cc13258996d2ab5cabe1cd8d12c5f54ba3 --- /dev/null +++ b/cport_cli/src/lib.rs @@ -0,0 +1,8 @@ +pub mod cli; + +pub mod i18n; +pub mod info; +pub mod install; +pub mod list; +pub mod remove; +pub mod service; diff --git a/cport_cli/src/list.rs b/cport_cli/src/list.rs new file mode 100644 index 0000000000000000000000000000000000000000..7856db320d81a84074e502a0b8e9892a1ae71961 --- /dev/null +++ b/cport_cli/src/list.rs @@ -0,0 +1,81 @@ +use colored::Colorize; +use libcport::consts::METADATA_FILE; +use libcport::errors::CportError; +use libcport::metadata::Metadata; +use libcport::port::Port; +use libcport::port::PortConfig; +use libcport::traits::TomlConfig; +use serde_json; + +use crate::fl; + +pub struct List { + metadata: Metadata, +} + +impl List { + pub fn new() -> Result { + let metadata = Metadata::parse(METADATA_FILE)?; + Ok(Self { metadata }) + } + + pub fn print_all_raw(&self, versions: bool) { + for port in &self.metadata.port_sys.ports { + if versions { + println!("{}-{}", &port.name, &port.version.dimmed()); + } else { + println!("{}", &port.name); + } + } + } + + pub fn print_normal_raw(&self, versions: bool) { + for port in &self.metadata.port_sys.ports { + let port = Port::new(&port.name); + if let Ok(pconf) = PortConfig::from_port(&port) { + if versions { + println!("{}-{}", &port.name, &pconf.package.version.dimmed()); + } else { + println!("{}", &port.name); + } + } + } + } + + pub fn print_all_formatted(&self) -> Result<(), CportError> { + for port in &self.metadata.port_sys.ports { + let name = port.name.rsplit_once('/').unwrap(); + print!("{}/{} ", name.0.bold().purple(), name.1.bold().white(),); + let prt = Port::new(&port.name); + if let Ok(pconf) = PortConfig::from_port(&prt) { + println!("{}", pconf.package.version.bold().green()); + println!(" {}", pconf.package.description.dimmed()); + } else { + println!("{}", port.version.bold().green()); + println!(" {}", fl!("get-description-failed").dimmed()); + } + } + Ok(()) + } + + pub fn print_normal_formatted(&self, versions: bool) { + for port in &self.metadata.port_sys.ports { + let port = Port::new(&port.name); + if let Ok(pconf) = PortConfig::from_port(&port) { + let name = port.name.rsplit_once('/').unwrap(); + print!("{}/{} ", name.0.bold().purple(), name.1.bold().white()); + if versions { + println!("{}", pconf.package.version.bold().green()); + } + println!(" {}", pconf.package.description.dimmed()); + } + } + } + + pub fn print_all_json(&self) -> Result<(), CportError> { + let string = serde_json::to_string(&self.metadata.port_sys.ports) + .map_err(|err| CportError::Serialize(err.to_string()))?; + println!("{string}"); + Ok(()) + } +} diff --git a/cport_cli/src/remove.rs b/cport_cli/src/remove.rs new file mode 100644 index 0000000000000000000000000000000000000000..5dba0eb655fe088a5898619a999b5a88aa1083bb --- /dev/null +++ b/cport_cli/src/remove.rs @@ -0,0 +1,73 @@ +use colored::Colorize; + +use libcport::errors::CportError; +use libcport::port::Port; +use libcport::port::PortConfig; +use libcport::remove::Remove; +use libcport::traits::TomlConfig; +use libmsg::Message; + +use crate::fl; +use crate::info::print_summary; +use crate::info::Operation; + +fn remove_port(port: &Port, dest: &str, verbose: bool) -> Result<(), CportError> { + let remove = Remove::new(port, dest); + remove + .remove_files(verbose) + .map_err(|err| CportError::RemoveFileDir(err.to_string()))?; + + Ok(()) +} + +pub fn remove(ports: &[String], dest: &str, verbose: bool, yes: bool) -> Result { + let msg = Message::default(); + let mut inst_ports = vec![]; + for port in ports { + // Работа `remove()` завершится, если какого-то порта нет + // или экземпляр `libcport::port::Port` по иным причинам + // невозможно создать. В предыдущем варианте функция + // всё равно продолжала свою работу, не уведомляя человека + // о наличии ошибок. + inst_ports.push(Port::try_new(port)?); + } + + // Check port priority + let mut is_error_exit = false; + let mut error_ports = vec![]; + + for port in &inst_ports { + let pconf = PortConfig::from_port(port)?; + if &pconf.package.priority == "system" { + is_error_exit = true; + error_ports.push(&port.name); + } + } + + if is_error_exit { + msg.error(fl!("invalid-action"), false); + msg.error(fl!("process-aborted"), false); + let ports_str = format!("{:?}", error_ports); + return Err(CportError::RemovePort( + ports_str, + fl!("sys-port-delete-fail"), + )); + } + + print_summary(&inst_ports, Operation::Remove)?; + if !yes + && !msg + .dialog(fl!("continue-removing"), false) + .map_err(|err| CportError::ReadFile(err.to_string()))? + { + return Err(CportError::Run(fl!("program-exit"))); + } + + // Remove ports + for port in &inst_ports { + msg.msg(fl!("remove-port", port_name = port.name.bold().to_string())); + remove_port(port, dest, verbose)?; + } + + Ok(0) +} diff --git a/cport_cli/src/service.rs b/cport_cli/src/service.rs new file mode 100644 index 0000000000000000000000000000000000000000..04494e09bdd20734b1eddca8dee3b1469a9ff246 --- /dev/null +++ b/cport_cli/src/service.rs @@ -0,0 +1,10 @@ +use libcport::db::Database; +use libcport::errors::CportError; +use std::path::Path; + +pub fn generate_database>(dest: P) -> Result<(), CportError> { + let db_path = dest.as_ref().join("var/lib/Calmira/software.db"); + let db = Database::create(db_path)?; + db.conn.close().map_err(|err| CportError::DatabaseError(err.1.to_string()))?; + Ok(()) +} diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 9c915625e282d9ac14f2ce0a7bdacf5d1f477a07..0000000000000000000000000000000000000000 --- a/docs/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# cport - -cport - это менеджер системы портов дистрибутива Calmira GNU/Linux-libre. В его -возможности входит установка и удаление портов, просмотр информации о -программном обеспечении, которое присутствует в портах, а также обновление -системы портов. Утилита написана на Python и использует SQLite3 для хранения -информации о портах. - -Этот менеджер системы портов решает многие проблемы в управлении программным -обеспечении в дистрибутиве Calmira GNU/Linux(-libre). В системе долгое -время не было утилиты для управления ПО, которая бы работала стабильно и -надёжно. Конечно, cport'у далеко до настоящей стабильности, но сейчас ведутся -работы по её повышению. - -## TODO - -- [x] Написание документации cport: - - [x] [Установка программного обеспечения;](install.md); - - [x] [Удаление программного обеспечения](remove.md); - - [x] [Просмотр информации о ПО](info.md); - - [x] [Обновление системы портов](update_ports.md); - - [x] [Обновление программного обеспечения](upgrade.md); -- [x] [Написание документации системы портов](https://gitlab.com/calmiralinux/cabs/Ports): - - [x] Краткий обзор системы портов; - - [x] Создание своего порта; diff --git a/docs/info.md b/docs/info.md deleted file mode 100644 index 8647d624263aedcb3ef757b5df6a10ea0fdb5a90..0000000000000000000000000000000000000000 --- a/docs/info.md +++ /dev/null @@ -1,35 +0,0 @@ -# Просмотр информации о портах - -Просмотр информации о программном обеспечении, которое присутствует в портах - -это очередная проблема, которую решает cport. Если ранее пользователю -приходилось запоминать, какая программа установлена, какой версии, а, самое -главное, для чего она нужна, то cport знает это для всего программного -обеспечения, которое представлено в портах. - -## Принцип работы - -В директории с системой портов содержится файл `port.toml`, который содержит 3 -секции: - -1. `package` - содержит информацию о пакете; -2. `deps` - содержит информацию о зависимостях этого пакета; -3. `port` - информация, необходимая только для cport - это ссылка для скачивания - архива с исходным кодом и MD5 сумма для проверки целостности скачивания. - -Выводятся сведения из секций `package` и `deps`. - -## Использование - -```bash -# Использование ключа '--info': -cport --info editors/gvim - -# Использование ключа '-I': -cport -I editors/gvim -``` - -При желании вы можете просматривать информацию о нескольких портах: - -```bash -cport --info editors/gvim base/acl base/binutils -``` diff --git a/docs/install.md b/docs/install.md deleted file mode 100644 index 852f6710f705a886d7305d7bfd9a207611beef58..0000000000000000000000000000000000000000 --- a/docs/install.md +++ /dev/null @@ -1,63 +0,0 @@ -# Установка портов - -Одна из самых важных функций, которую в значительной степени упрощает и -автоматизирует cport - это сборка пакета из исходного кода и установка его в -систему. Для корректной сборки пакета иногда приходится вводить достаточно -большое число команд в терминал, и далеко не все из этих команд короткие и/или -простые. cport же это всё упрощает тем, что все необходимые для сборки действия -записаны в BASH-скрипт, являющийся инструкцией по сборке. При желании или при -необходимости пользователь может отредактировать эти сборочные инструкции в -зависимости от целей и запросов. - -Для установки порта используется ключ `--install` (сокращённая версия: `-i`). -Далее этому ключу указывается порт или несколько портов, которые необходимо -собрать. - -Сначала менеджер системы портов выведет подробную информацию о программном -обеспечении, которое хочет собрать и установить пользователь, а потом запросит -подтверждение действий (нужно ввести `y` или `n` и нажать `Enter` для -продолжения или прекращения действий соотв.). В том случае, если пользователь -ответил утвердительно, начнётся скачивание архива с исходным кодом порта, его -распаковка (на данный момент поддерживаются только архивы tar) и выполнение -сборочных инструкций. - -Например: - -```bash -cport -i xorg/gtk3 editors/gvim -``` - -Эта команда установит порты `xorg/gtk3` и `editors/gvim`. - -Подтверждение вызывается для каждого действия. Т.е. если вы, например, указали -сборку одного порта, то у пользователя будет запрошено лишь одно подтверждение -действий. Если пользователь указал сборку десяти портов, то запрос о продолжении -действий будет задан десять раз. - -Если пользователь по каким-то причинам не сможет ответить на запросы, то он -может отключить их, указав ключ `--yes` (сокращённая версия: `-y`). В таком -случае запрос о продолжении действий не будет задан ни разу. Считайте, что -`cport` будет автоматически отвечать утвердительно на все запросы. - -Например: - -``` -cport -i editors/gvim -``` - -![](pic/cport1.png) - -Здесь мы видим, что после того, как была выведена информация о порте, cport -запрашивает пользователя, продолжить ли ему работу или нет. В том случае, если -пользователь введёт `y` и нажмёт `Enter`, то cport скачает архив с исходным -кодом порта, распакует его и приступит к сборке. - -``` -cport --yes -i editors/gvim -``` - -![](pic/cport2.png) - -Во втором случае мы указали ключ `--yes` (или его сокращённую версию `-y`). -Тогда cport не будет ничего запрашивать у пользователя и сразу присупит к -сборке. diff --git a/docs/pic/cport1.png b/docs/pic/cport1.png deleted file mode 100644 index acf381b5db00d5883ea8673be73447f3af9d14d6..0000000000000000000000000000000000000000 Binary files a/docs/pic/cport1.png and /dev/null differ diff --git a/docs/pic/cport2.png b/docs/pic/cport2.png deleted file mode 100644 index a0578fcbc8f6a9bf7915a121951d5eba827d067b..0000000000000000000000000000000000000000 Binary files a/docs/pic/cport2.png and /dev/null differ diff --git a/docs/remove.md b/docs/remove.md deleted file mode 100644 index 2c5513f3dcf4cfa0e01423bef3b170a5873956d5..0000000000000000000000000000000000000000 --- a/docs/remove.md +++ /dev/null @@ -1,48 +0,0 @@ -# Удаление программного обеспечения - -Удаление программного обеспечения - это одна из самых важных проблем, которую -решает cport. Если с установкой всё легко, то с удалением программного -обеспечения могут возникать всевозможные проблемы, так как на жёстком диске ПК -придётся хранить архив с исходным кодом пакета, в котором содержится `Makefile` -или какие-то подобные файлы, содержащие инструкции как по сборке и установке -пакета, так и для его удаления из системы. Но тут есть один маленький нюанс: не -все разработчики пакетов добавляют инструкции по удалению своего программного -обеспечения из системы. Тогда пользователю придётся решать эту проблему -самостоятельно: находить все файлы, которые принадлежат нужному пакету, и -вручную удалять их. - -Но что если пользователь не удалит какие-то файлы до конца? Что если -пользователь удалит не те файлы? Те, которые принадлежат другому пакету? В таком -случае он может вообще сломать либо какое-то другое ПО, либо вообще всю систему. -Для того, чтобы обезопасить пользователя, и была реализована функция удаления ПО -из системы средствами cport, а не средствами, про которые многие разработчики -забывают или принципиально не добавляют. - -## Принцип работы - -В директории с нужным портом содержится файл `files.list`. Он как раз содержит -список того, что требуется удалить. cport читает этот файл, разбивает его -содержимое на список (каждый файл в `files.list` указывается с новой строки) и -пофайлово из системы удаляет. - -## Использование cport - -Для удаления пакета с помощью cport используются ключи `--remove` и `-r`: - -```bash -cport -r editors/gvim -``` - -Вы можете указать несколько портов, тогда они будут последовательно удалены из -системы: сначала первый, потом второй и так далее: - -```bash -cport -r editors/gvim editors/vim general/git -``` - -> **Обратите внимание**, что cport удаляет только те порты, которые имеют -> пользовательский статус. Это значит, что все порты из категории `base` удалить -> вы не сможете. - -Кроме того, вы не сможете удалить порт, упоминание о котором отсутствует в базе -данных cport. diff --git a/docs/update_ports.md b/docs/update_ports.md deleted file mode 100644 index 6c9bb3fb2d1be1c60e8c19daf09496955fa22b96..0000000000000000000000000000000000000000 --- a/docs/update_ports.md +++ /dev/null @@ -1,36 +0,0 @@ -# Обновление системы портов - -Система портов всё время обновляется: туда добавляется новое программное -обеспечение, обновляется уже существующее и удаляется устаревшее. Для упрощения -и автоматизации установки обновления именно системы портов в cport используется -ключ `--update` или его сокращённая версия: `-u`. Версия системы портов -скачивается из той ветки, которая указана в параметре `repo.branch` файла -`/etc/cport.toml`. - -Использование: - -```bash -cport --update -# Или: -cport -u -``` - -В том случае, если обновлений нет, будет выведено следующее сообщение: - -``` -==> Downloading the ports system... -100% [......................................................................] 37532 / 37532 -==> Unpacking ports in '/usr/src'... -==> Generating list of changes... -[!] No updates. -``` - -В противном случае будут выведены таблицы, показывающие список изменений в новой -версии портов и в той, которая установлена в системе: - -- Какие порты были удалены и/или добавлены; -- Какие порты были обновлены (таблица из трёх колонок - название, старая и новая - версии). - -> Для работы модуля обновлений системы портов требуется, чтобы размер терминала -> был не менее 80 колонок (символов в ширину). diff --git a/docs/upgrade.md b/docs/upgrade.md deleted file mode 100644 index 8ec3534417731fe9188daa1f028c6383e6af6200..0000000000000000000000000000000000000000 --- a/docs/upgrade.md +++ /dev/null @@ -1,36 +0,0 @@ -# Обновление программного обеспечения - -cport поддерживает двухрежимное обновление программного обеспечения до новой -версии (в случае, если это ПО, конечно, установлено в систему). Режимы `soft` и -`hard`. - -## `soft` - -В первом случае новая версия порта просто устанавливается поверх старой и в базе -данных установленного ПО обновляются сведения о порте. - -## `hard` - -Порт удаляется из базы данных и из системы, после чего устанавливается новая -версия порта. Данный функционал не рекомендуется использовать для системного ПО -или компиляторов. По сути, ПО просто переустанавливается. - -## Проведение обновления ПО - -Для начала актуализируйте систему портов: - -```bash -cport -u -``` - -В выводе будут указаны все порты, в которых произошли изменения. Если порт, -который вы хотите обновить, присутствует в списке, то используйте опцию `-U` для -обновления: - -```bash -cport -U [список портов, которые требуется обновить до новой версии] -``` - -Если версия установленного порта не соответствует версии присутствующего в -системе портов, то начнётся обновление. В противном случае обновление завершится -с ошибкой из-за совпадения версий портов. diff --git a/libcport/Cargo.toml b/libcport/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..a27b39c0b06dcec4741d8a05a1d12e15799ba51e --- /dev/null +++ b/libcport/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "libcport" +version = "2.1.1" +edition = "2021" +description = "Library for managing the Calmira GNU/Linux-libre port system" +authors = ["Michail Krasnov "] +license = "GPL-3.0-or-later" + +[dependencies] +anyhow.workspace = true +chrono.workspace = true +futures.workspace = true +futures-util.workspace = true +indicatif.workspace = true +reqwest.workspace = true +libmsg.workspace = true +rusqlite.workspace = true +serde.workspace = true +thiserror.workspace = true +tokio.workspace = true +toml.workspace = true diff --git a/libcport/README.md b/libcport/README.md new file mode 100644 index 0000000000000000000000000000000000000000..62772e0e7dcc6dfff6f6b0663039ff7a95ebf01e --- /dev/null +++ b/libcport/README.md @@ -0,0 +1,13 @@ +# libcport + +`libcport` is a library for managing the Calmira GNU/Linux-libre port system. It provides an API for installing and removing ports, maintaining them (creating, removing, updating) and getting information about ports. + +It also provides a means of protecting the system from breakdowns resulting from conflicts between multiple ports or from erroneous deletion of another port's files. + + + +## License + +GNU GPLv3.0 or later. diff --git a/libcport/examples/new_port_inst.rs b/libcport/examples/new_port_inst.rs new file mode 100644 index 0000000000000000000000000000000000000000..897e6c06bf1db3e85b3bf8f328cfdc068759ebb0 --- /dev/null +++ b/libcport/examples/new_port_inst.rs @@ -0,0 +1,11 @@ +use libcport::port::Port; +use libcport::port::PortConfig; +use libcport::traits::TomlConfig; + +fn main() { + let port = Port::new("base/acl"); + if port.dir_exists() { + let port_toml = PortConfig::from_port(&port).unwrap(); + println!("{:#?}", port_toml); + } +} diff --git a/libcport/src/consts.rs b/libcport/src/consts.rs new file mode 100644 index 0000000000000000000000000000000000000000..1ebf43b7ea4e4a31e54086c3f43d66362b18f876 --- /dev/null +++ b/libcport/src/consts.rs @@ -0,0 +1,12 @@ +//! Constants used inside `(lib)cport` + +pub const PORTS_DIR: &str = "/usr/ports"; +pub const TEST_PNAME: &str = "general/test_prt"; +pub const DATABASE: &str = "/var/lib/Calmira/software.db"; +pub const CACHE_DIR: &str = "/var/cache/cport"; +pub const LOG_DIR: &str = "/var/log/cport"; +pub const CONF_FILE: &str = "/etc/cport.conf"; +pub const SRC_DIR: &str = "/usr/src"; +pub const INTERPRETER: &str = "/bin/bash"; +pub const INSTALL_SCRIPT: &str = "/usr/share/cport/install.sh"; +pub const METADATA_FILE: &str = "/usr/ports/metadata.toml"; diff --git a/libcport/src/db.rs b/libcport/src/db.rs new file mode 100644 index 0000000000000000000000000000000000000000..f2ef397cd0cbbad862e0d2c80c730ecf3a9d1d69 --- /dev/null +++ b/libcport/src/db.rs @@ -0,0 +1,279 @@ +//! Module for work with database + +use crate::errors::CportError; +use crate::port::Port; +use rusqlite::Connection; +use std::path::Path; + +/// Type for working with the database +pub struct Database { + pub conn: Connection, + + /// Number of operations performed + pub oper: usize, +} + +/// Contents of table `ports` +#[derive(Debug, PartialEq)] +pub struct Installed { + /// Port name + pub name: String, + + /// Port version + pub version: String, + + /// Space occupied by the port in the file system + pub usage: f64, + + /// Port build time (sec. / 3.15 min.) + pub build_time: f64, +} + +/// Contents of table `files` +#[derive(Debug)] +pub struct File { + /// Path to port file in the filesystem + /// + /// **Example:** `/usr/share/some_package/license.txt` + pub path: String, + + /// The name of the port to which this file owns + /// + /// **Example:** `base/acl` + pub port_name: String, +} + +impl Database { + /// Creates database and opens connection to it + pub fn create>(path: P) -> Result { + let conn = + Connection::open(path).map_err(|err| CportError::DatabaseError(err.to_string()))?; + + let query = "CREATE TABLE IF NOT EXISTS ports (\ + name TEXT NOT NULL UNIQUE, \ + version TEXT NOT NULL, \ + usage REAL NOT NULL, \ + build_time REAL NOT NULL \ + ); \ + CREATE TABLE IF NOT EXISTS files ( \ + path TEXT NOT NULL, \ + port_name TEXT NOT NULL, \ + );"; + + conn.execute(query, []) + .map_err(|err| CportError::DatabaseError(err.to_string()))?; + + Ok(Self { conn, oper: 1 }) + } + + /// Opens connection to **existing** database + pub fn open>(path: P) -> Result { + let conn = + Connection::open(path).map_err(|err| CportError::DatabaseError(err.to_string()))?; + Ok(Self { conn, oper: 1 }) + } + + /// Gets information about installed port + pub fn get_port(&self, port: &Port) -> Result, CportError> { + let query = "SELECT * FROM ports WHERE name = ?1"; + let mut stmt = self + .conn + .prepare(query) + .map_err(|err| CportError::DatabaseError(err.to_string()))?; + let ports = stmt + .query_map([&port.name], |row| { + Ok(Installed { + name: row.get(0)?, + version: row.get(1)?, + usage: row.get(2)?, + build_time: row.get(3)?, + }) + }) + .map_err(|err| CportError::DatabaseError(err.to_string()))?; + + let mut rows = Vec::new(); + for port in ports { + rows.push(port.map_err(|err| CportError::DatabaseError(err.to_string()))?); + } + + Ok(rows) + } + + pub fn get_file(&self, file: &str) -> Result, CportError> { + let query = "SELECT * FROM files WHERE path = ?1"; + let mut stmt = self + .conn + .prepare(query) + .map_err(|err| CportError::DatabaseError(err.to_string()))?; + let files = stmt + .query_map([file], |row| { + Ok(File { + path: row.get(0)?, + port_name: row.get(1)?, + }) + }) + .map_err(|err| CportError::DatabaseError(err.to_string()))?; + + let mut rows = Vec::new(); + for file in files { + rows.push(file.map_err(|err| CportError::DatabaseError(err.to_string()))?); + } + + Ok(rows) + } + + /// Adds a port entry to the database + pub fn add_port(&mut self, port: &Installed) -> Result<(), CportError> { + let query = "INSERT INTO ports VALUES (?1, ?2, ?3, ?4);"; + + let oper = self + .conn + .execute( + query, + [ + &port.name, + &port.version, + &port.usage.to_string(), + &port.build_time.to_string(), + ], + ) + .map_err(|err| CportError::DatabaseError(err.to_string()))?; + + self.oper += oper; + Ok(()) + } + + pub fn add_file(&mut self, file: &File) -> Result<(), CportError> { + let query = "INSERT INTO files VALUES (?1, ?2)"; + + let oper = self + .conn + .execute(query, [&file.path, &file.port_name]) + .map_err(|err| CportError::DatabaseError(err.to_string()))?; + self.oper += oper; + Ok(()) + } + + /// Removes a port entry + pub fn remove_port(&mut self, port: &Installed) -> Result<(), CportError> { + let query = "DELETE FROM ports WHERE name = ?1"; + + let oper = self + .conn + .execute(query, [&port.name]) + .map_err(|err| CportError::DatabaseError(err.to_string()))?; + + self.oper += oper; + Ok(()) + } + + pub fn remove_file(&mut self, file: &File) -> Result<(), CportError> { + let query = "DELETE FROM files WHERE path = ?1"; + + let oper = self + .conn + .execute(query, [&file.path]) + .map_err(|err| CportError::DatabaseError(err.to_string()))?; + + self.oper += oper; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::consts::TEST_PNAME; + + const TEST_DB: &str = "./tests/test.db"; + + #[test] + fn create_test() { + let conn = Database::create("./test.db").unwrap(); + conn.conn.close().unwrap(); + assert_eq!(conn.oper, 1, "One operation must be performed"); + } + + #[test] + fn open_test() { + let db_pth = Path::new("./test.db"); + + if !db_pth.exists() { + let _conn = Database::create(db_pth).unwrap(); + _conn.conn.close().unwrap(); + } + + let conn = Database::open(db_pth).unwrap(); + conn.conn.close().unwrap(); + assert_eq!(conn.oper, 1, "One operation must be performed"); + } + + #[test] + fn get_port_test() { + let conn = Database::open(TEST_DB).unwrap(); + let pkg = vec![Installed { + name: TEST_PNAME.to_string(), + version: "0.1".to_string(), + usage: 666.0, + build_time: 666.0, + }]; + let port = Port::new(TEST_PNAME); + + let pkg_from_db = conn.get_port(&port).unwrap(); + conn.conn.close().unwrap(); + + assert_eq!(pkg_from_db.len(), 1); + assert_eq!(pkg_from_db[0], pkg[0]); + } + + #[test] + fn add_port_test() { + let mut conn = Database::create("./test.db").unwrap(); + let pkg = Installed { + name: TEST_PNAME.to_string(), + version: "0.1".to_string(), + usage: 666.0, + build_time: 666.0, + }; + let port = Port::new(TEST_PNAME); + + conn.add_port(&pkg).unwrap(); + let pkg_from_db = conn.get_port(&port).unwrap(); + + conn.conn.close().unwrap(); + + assert_eq!(conn.oper, 2); + assert_eq!(pkg_from_db, vec![pkg]); + } + + #[test] + fn remove_port_test() { + let mut conn = Database::create("./test.db").unwrap(); + let pkg = Installed { + name: TEST_PNAME.to_string(), + version: "0.1".to_string(), + usage: 666.0, + build_time: 666.0, + }; + let port = Port::new(TEST_PNAME); + + let pkg_from_db = conn.get_port(&port).unwrap(); + if !pkg_from_db.is_empty() { + conn.remove_port(&pkg).unwrap(); + } + + conn.add_port(&pkg).unwrap(); + let pkg_from_db = conn.get_port(&port).unwrap(); + + assert_eq!(conn.oper, 2); + assert_eq!(pkg_from_db.len(), 1); + + conn.remove_port(&pkg).unwrap(); + let pkg_from_db = conn.get_port(&port).unwrap(); + + assert_eq!(conn.oper, 3); + assert_eq!(pkg_from_db.len(), 0); + + conn.conn.close().unwrap(); + } +} diff --git a/libcport/src/errors.rs b/libcport/src/errors.rs new file mode 100644 index 0000000000000000000000000000000000000000..c7202106e5254803817f4d04a38db86071dd0710 --- /dev/null +++ b/libcport/src/errors.rs @@ -0,0 +1,85 @@ +//! Handling `cport` specific errors + +use thiserror::Error; + +/// Contains `cport` error types +#[derive(Debug, Error)] +pub enum CportError { + /// Catalog creation error + #[error("failed to create directory: {0}")] + CreateDir(String), + + /// File creation error + #[error("failed to create file: {0}")] + CreateFile(String), + + /// File read error + #[error("failed to read file: {0}")] + ReadFile(String), + + /// IO Error + /// + /// ## Values: + /// + /// 1. Operation; + /// 2. File/directory name; + /// 3. Error string; + #[error("input/output error (op: {0}): file/dir {1}, error: {2}")] + IO(String, String, String), + + /// Remove error + #[error("failed to remove file or directory: {0}")] + RemoveFileDir(String), + + /// Error writing configuration file or other data + #[error("failed to serialize object cause: {0}")] + Serialize(String), + + /// Error reading/parsing configuration file or other data + #[error("failed to deserialize object cause: {0}")] + Deserialize(String), + + /// The requested port does not exist on the file system + #[error("port '{0}' does not exist")] + PortNotFound(String), + + /// The port config file does not have a `deps` section + #[error("port '{0}' does not have a `deps` section")] + PortDepsNotFound(String), + + /// There is no file in the port directory marked as needed or requested in `cport` + #[error("file '{0}' from port '{1}' does not exist")] + PortFileNotFound(String, String), + + /// Port Deletion Error. It is possible that it has system priority or is not available in the database + #[error("port(s) '{0}' deletion error: {1}")] + RemovePort(String, String), + + /// Error downloading a file from remote sources + #[error("file download error: {0}")] + DownloadFile(String), + + /// The program or script returned an unsuccessful exit code + #[error("program or script exited with code other than zero: {0}")] + RunCode(i32), + + /// Program or script execution error + #[error("execution error: {0}")] + Run(String), + + /// SQL query execution error + #[error("database error: {0}")] + DatabaseError(String), + + #[error("git error: {0}")] + GitError(String), + + #[error("requested data of parameter '{0}' is missing")] + DataMissing(String), + + #[error("the format of the requested data ('{0}') of parameter '{1}' is incorrect")] + DataError(String, String), + + #[error("the format of the requested data ('{0}') of parameter '{1}' is incorrect\n\nUnsupported chars: {2}")] + DataError2(String, String, String), +} diff --git a/libcport/src/install.rs b/libcport/src/install.rs new file mode 100644 index 0000000000000000000000000000000000000000..1cbe53daf55da42b4dc054fd615d5ee90430cdc9 --- /dev/null +++ b/libcport/src/install.rs @@ -0,0 +1,213 @@ +//! Functions for the execution of build instructions of the port + +use crate::consts::INSTALL_SCRIPT; +use crate::consts::INTERPRETER; +use crate::port::Port; +use crate::port::PortConfig; +use crate::traits::TomlConfig; + +use anyhow::Context; +use anyhow::Result; +use std::cmp::min; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use std::process::Command; +use std::time::SystemTime; + +use futures_util::StreamExt; +use indicatif::ProgressBar; +use indicatif::ProgressStyle; +use reqwest; +use tokio; + +/// The structure describes the result of building a package +/// +/// The build result is the code returned by the package +/// build script that was executed in a certain time, in seconds +#[derive(Debug)] +pub struct BuildResult { + /// Returned value + pub code: i32, + + /// Build time (sec.) + pub time: f64, +} + +impl BuildResult { + pub fn new(code: i32, time: f64) -> Self { + Self { code, time } + } + + /// Presents the port build time as SBU (Standard Build Unit) from LFS + /// + /// The main difference of SBUs in `cport` is that here the build time + /// of the first packet is not taken as 1 SBU. Here the build time is + /// **always** divided by 189 - historically, the build time of the + /// first package on the creator of the Calmira GNU/Linux-libre + /// distribution was this value. + pub fn time_as_sbu(&self) -> f64 { + self.time / 189. + } +} + +/// Contains definitions of environment variables passed to the port +/// build instructions +/// +/// ## Examples +/// ``` +/// use libcport::install::Install; +/// use libcport::port::Port; +/// +/// let port = Port::new("base/acl"); +/// let install = Install::new( +/// &port, /* port name */ +/// 4, /* threads */ +/// "/usr/src", /* directory with source code */ +/// "/opt/program", /* installation prefix */ +/// false, /* Whether to test a port */ +/// true, /* verbose mode */ +/// ); +/// +/// install.run_inst_script().unwrap(); +/// ``` +pub struct Install<'a> { + /// Port name + port: &'a Port, + + /// Number of build threads + threads: u8, + + /// Port source code directory + src_dir: &'a str, + + /// Path in the file system where the port will be installed + prefix: &'a str, + + /// Whether to test the port or not + test: bool, + + verbose: bool, +} + +impl<'a> Install<'a> { + pub fn new( + port: &'a Port, + threads: u8, + src_dir: &'a str, + prefix: &'a str, + test: bool, + verbose: bool, + ) -> Self { + Self { + port, + src_dir, + prefix, + threads, + test, + verbose, + } + } + + /// Downloads the archive with the port source code + #[tokio::main] + pub async fn download(&self) -> Result<()> { + let pconf = PortConfig::from_port(self.port)?; + let port_section = match &pconf.port { + Some(section) => section, + None => return Err(anyhow::anyhow!("[port] section not found")), + }; + let url = match &port_section.url { + Some(u) => u, + None => return Err(anyhow::anyhow!("port.url parameter not found")), + }; + let fname = pconf + .get_downloaded_fname() + .context("port.{url,file}: unable to determine downloaded file name")?; + + let res = reqwest::get(url).await?; + let total_size = res.content_length().unwrap_or(0); + + let pb = ProgressBar::new(total_size); + pb.set_style( + ProgressStyle::default_bar() + .template("{msg} [{bar}] {bytes}/{total_bytes} {elapsed}/{eta}")? + .progress_chars("=> "), + ); + pb.set_message(format!("Download '{}'", &self.port.name)); + + let file_name = Path::new(&self.src_dir).join(fname); + let mut dest = File::create(&file_name)?; + + let mut downloaded: u64 = 0; + let mut stream = res.bytes_stream(); + + while let Some(item) = stream.next().await { + let chunk = item?; + dest.write_all(&chunk)?; + let new = min(downloaded + (chunk.len() as u64), total_size); + downloaded = new; + pb.set_position(new); + } + + pb.finish(); + + Ok(()) + } + + /// Executes port building instructions + pub fn run_inst_script(&self) -> Result { + let time = SystemTime::now(); + let envs = [ + ("CPORT_INSTALLATION_PREFIX", self.prefix), + ("CPORT_BUILD_THREADS", &self.threads.to_string()), + ("PORT_DIR", &self.port.port_pth), + ("SRC_DIR", self.src_dir), + ("IS_TEST", if self.test { "YES" } else { "NO" }), + ]; + + let command = if self.verbose { + Command::new(INTERPRETER) + .arg("-c") + .arg(INSTALL_SCRIPT) + .envs(envs) + .status()? + } else { + let log_pth = format!("/tmp/{}.cport_build.log", &self.port.name.replace('/', "_")); + let f = File::options().append(true).create(true).open(log_pth)?; + + Command::new(INTERPRETER) + .arg("-c") + .arg(INSTALL_SCRIPT) + .envs(envs) + .stdout(f) + .output()? + .status + }; + + let code = command.code().unwrap_or(0); + let time_elapsed = match time.elapsed() { + Ok(elapsed) => elapsed.as_secs_f64(), + Err(_) => 0., + }; + + Ok(BuildResult::new(code, time_elapsed)) + } +} + +#[cfg(test)] +mod tests { + use crate::consts::TEST_PNAME; + + use super::*; + + #[test] + fn run_inst_script() { + let port = Port::new(TEST_PNAME); + let inst = Install::new(&port, 4, "/usr/src", "/tmp/vagina", false, false); + let code = inst.run_inst_script().unwrap(); + + assert_eq!(code.code, 0); + assert_eq!(code.time.round(), 1.); + } +} diff --git a/libcport/src/install.sh b/libcport/src/install.sh new file mode 100755 index 0000000000000000000000000000000000000000..f62d20e2951d099d4da6c300bc6bff04f18063be --- /dev/null +++ b/libcport/src/install.sh @@ -0,0 +1,157 @@ +#!/bin/bash -e +# Build provider for `libcport` library +# (C) 2023 Michail Krasnov + +############################################################################### +## ENVIRONMENT VARIABLES: +## + +# - 'CPORT_BUILD_THREADS' - число потоков сборки +# - 'CPORT_INSTALLATION_PREFIX' - префикс установки порта +# - 'SRC_DIR' - путь до исходного кода порта +# - 'PORT_DIR' - путь до директории с файлами порта ('install', 'port.toml', etc.) +# - 'IS_TEST' - тестировать ли порт после сборки + +export MAKEFLAGS="-j${CPORT_BUILD_THREADS}" +export NINJAJOBS="${CPORT_BUILD_THREADS}" +export DESTDIR="${CPORT_INSTALLATION_PREFIX}" + +export LOG_FILE="/var/log/cport.log" + +############################################################################### +## FUNCTIONS: +## + +log_msg() { + echo "[ $(date) ] [libcport_build_provider] [user: $(whoami) ] $1" >> $LOG_FILE +} + +# print_msg() { +# msg=$1 +# echo -e "\e[1;33m===>\e[0m $msg" +# log_msg $msg +# } + +# print_msg1() { +# msg=$1 +# echo -e " \e[1;33m-->\e[0m $msg" +# log_msg $msg +# } + +# err_msg() { +# msg=$1 +# echo -e "\e[1;31m[libcport] [error]\e[0m $msg" +# log_msg "[provider error] $msg" +# } + +check_status() { + CODE=$1 + if [ $CODE != 0 ]; then + cmsg error "Error with '$2': $CODE returned!" + exit $CODE + else + cmsg msg2 "Successful." + fi +} + +clean_src() { + cd $SRC_DIR + log_msg "Cleaning the source code tree from temporary directories..." + for fname in *; do + if [ -d $fname ]; then + rm -rf $fname + fi + done +} + +############################################################################### +## BUILD INSTRUCTIONS: +## + +log_msg "Connectiong build instructions" + +. $PORT_DIR/install + +log_msg "List of ALL environment variables:" +env >> $LOG_FILE +echo -e "\n" >> $LOG_FILE + +cd $SRC_DIR + +if [ -z $ARCHIVE ]; then + cmsg msg "Extracting the package source code..." + tar -xf ${NAME}-${VERSION}.* + cmsg msg2 "Extracting successful." +else + if [[ -n $ARCHIVE && $ARCHIVE != "none" ]]; then + cmsg msg "Extracting the package source code..." + tar -xf $ARCHIVE + cmsg msg2 "Extracting successful." + fi +fi + +if [ -z $BUILD_DIR ]; then + cmsg msg "Switch to the working directory..." + cd ${NAME}-${VERSION} + cmsg msg2 "Switch successful." +else + if [[ ! -z $BUILD_DIR && $BUILD_DIR != "none" ]]; then + cmsg msg "Switch to the working directory..." + cd $BUILD_DIR + cmsg msg2 "Switch successful." + fi +fi + +if [ "$(type -t 'prepare')" == "function" ]; then + cmsg msg "Executing the 'prepare()' function..." + prepare + check_status $? 'prepare()' +fi + +if [ "$(type -t 'build')" == "function" ]; then + cmsg msg "Executing the 'build()' function..." + build + check_status $? 'build()' +fi + +if [[ "$(type -t 'test')" == "function" && $IS_TEST == "YES" ]]; then + cmsg msg "Executing the 'test()' function..." + log_name="$(date)-test-port.log" + test | tee /var/log/cport/$log_name + CODE=$? + + echo -e "\n\n\e[1mTEST LOG FILE:\e[0m /var/log/cport/$log_file\n" + echo -e -n "\e[1m::\e[0m Continue building (y/n)? " + read run + + if [[ $run == "n" || $run == "N" ]]; then + cmsg error "Tests was returned code $CODE! Cancel building..." + exit $CODE + else + cmsg warn "\nTests was returned code $CODE! \e[1mContinue building...\e[0m\n\n" + fi + + unset CODE log_file run +fi + +if [ "$(type -t 'preinst')" == "function" ]; then + cmsg msg "Executing the 'preinst()' function..." + preinst + check_status $? 'preinst()' +fi + +if [ "$(type -t 'pkginst')" == "function" ]; then + cmsg msg "Executing the 'pkginst()' function..." + pkginst + check_status $? 'pkginst()' +fi + +if [ "$(type -t 'postinst')" == "function" ]; then + cmsg msg "Executing the 'postinst()' function..." + postinst + check_status $? 'postinst()' +fi + +# ldconfig +# clean_src +exit 0 diff --git a/libcport/src/lib.rs b/libcport/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..e41f30234b9bc8b92536f53b43a3e8193fc547a7 --- /dev/null +++ b/libcport/src/lib.rs @@ -0,0 +1,36 @@ +//! # libcport +//! +//! `libcport` is a library for managing the Calmira GNU/Linux-libre port system. It provides an API for +//! installing and removing ports, maintaining them (creating, removing, updating) and +//! getting information about ports. +//! +//! It also provides a means of protecting the system from breakdowns resulting from +//! conflicts between multiple ports or from erroneous deletion of another port's files. +//! +//! ## Base abstractions +//! +//! - `libcport::port::Port` - is the basic data type for working with the port. +//! Provides methods for creating and deleting a port from the port system, checking the +//! port for correctness, or getting information about the port. +//! - ... +//! +//! ## Author +//! +//! Michail Krasnov, 2021-2023. +//! +//! > **ATTENTION:** 2202 2062 5233 5406 (Sberbank) - for donats. +//! +//! ## License +//! +//! GNU GPLv3.0 or later. + +pub mod consts; +pub mod db; +pub mod errors; +pub mod metadata; +pub mod port; +pub mod traits; +pub mod utils; + +pub mod install; +pub mod remove; diff --git a/libcport/src/metadata.rs b/libcport/src/metadata.rs new file mode 100644 index 0000000000000000000000000000000000000000..6da994f4a3684bc7096e35e86c304d989e4fd4ab --- /dev/null +++ b/libcport/src/metadata.rs @@ -0,0 +1,226 @@ +//! Module for parsing port system metadata + +use anyhow::Result; +use chrono::DateTime; +use chrono::Utc; +use serde::Deserialize; +use serde::Serialize; +use std::cmp::Ordering; +use std::fs; +use std::path::Path; + +use crate::port::Port; +use crate::port::PortConfig; +use crate::traits::TomlConfig; + +/// Parameter definitions in the metadata file +/// +/// ## Example file +/// +/// > **Warning:** this file is not for human reading. Only software can +/// > manipulate it using the `libcport` library. +/// +/// ```no-test +/// [port_sys] +/// categories = ["base", "bs/cross_compiler", "bs/prepare", "general", "xorg"] +/// +/// [[port_sys.ports]] +/// name = "base/openrc" +/// version = "2.0" +/// modified = "Tue, 22 Aug 2023 14:00:47 +0000" +/// +/// [[port_sys.ports]] +/// name = "base/patch" +/// version = "1.1.4" +/// modified = "Tue, 22 Aug 2023 14:00:47 +0000" +/// +/// ... +/// +/// [system] +/// releases = ["v2.0", "v2.1", "v2.2", "v2.3"] +/// ``` +#[derive(Debug, Deserialize, Serialize)] +pub struct Metadata { + /// Information about ports + pub port_sys: PortSys, + + /// Information about port compatibility with the Calmira + /// GNU/Linux-libre distribution + pub system: System, +} + +/// List of categories and ports +#[derive(Debug, Deserialize, Serialize)] +pub struct PortSys { + /// A human-specified list of categories in which to search for ports + pub categories: Vec, + + /// List of all ports found + pub ports: Vec, +} + +/// Information about ports (name, version, creation/modification date) +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq)] +pub struct Ports { + /// Port name (from [`Port`]) + pub name: String, + + /// Port version (from `package.version`, [`PortConfig`]) + pub version: String, + + /// Creation/modification date (rfc2822) + pub modified: String, +} + +/// Information about port compatibility with the Calmira GNU/Linux-libre distribution +#[derive(Debug, Deserialize, Serialize)] +pub struct System { + /// List of Calmira GNU/Linux-libre versions that this ports system is compatible with + pub releases: Vec, +} + +impl Ord for Ports { + fn cmp(&self, other: &Self) -> Ordering { + self.name.cmp(&other.name) + } +} + +impl PartialOrd for Ports { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.name.cmp(&other.name)) + } +} + +impl Metadata { + /// Scans the ports system directory and generates a list of software + /// + /// > **Warning:** this method completely erases the previous software + /// > list and generates a new one. All port change data (creation or + /// > modification) will be changed. + /// + /// > **NOTE:** The `scan()` method should get port version information + /// > from its `port.toml` file, but it currently does not. This + /// > functionality will be implemented after the port system is fully + /// > migrated to the new version of the standard. + pub fn scan>(&mut self, ports_path: P) -> Result<()> { + let categories = &self.port_sys.categories; + let mut ports = vec![]; + + for category in categories { + let ports_pth = Path::new(ports_path.as_ref()).join(category); + let dir_content = fs::read_dir(&ports_pth)?; + + for file in dir_content.filter_map(|f| f.ok()) { + let md = file.metadata()?; + if md.is_dir() { + let port_name = format!( + "{}/{}", + category, + file.file_name().into_string().unwrap_or_default() + ); + let port = Port::new(&port_name).set_path(&ports_path); + if port.dir_exists() { + let port_md = match PortConfig::from_port(&port) { + Ok(pconf) => { + let time_now: DateTime = Utc::now(); + let modified = time_now.to_rfc2822(); + Ports { + name: port_name.clone(), + version: pconf.package.version.clone(), + modified, + } + } + Err(_) => Ports { + name: port_name.clone(), + version: "???".to_string(), + modified: "0".to_string(), + }, + }; + + if let Some(search_result) = self.search_port(&port_md.name) { + if search_result.version != *port_md.version { + ports.push(search_result); + } + } else { + ports.push(port_md); + } + } + } + } + } + ports.sort_unstable(); + self.port_sys.ports = ports; + Ok(()) + } + + /// Adds the port to the software list + /// + /// > **Warning:** this method adds the port to the *end* of the list, + /// > without sorting it or changing the order of other ports. In case + /// > you need to sort a vector with ports, use the [`Vec`] + /// > functionality for that (methods `sort()` and `sort_unstable()` + /// > for example). + pub fn add_port(&mut self, port: &Port) -> Result<()> { + let pconf = PortConfig::from_port(port)?; + let time_now: DateTime = Utc::now(); + let created = time_now.to_rfc2822(); + + let port_md = Ports { + name: port.name.to_string(), + version: pconf.package.version.to_string(), + modified: created, + }; + + self.port_sys.ports.push(port_md); + Ok(()) + } + + /// Method for search port in the metadata by port name + pub fn search_port(&self, pname: &str) -> Option { + let mut is_asc = true; + if self.port_sys.ports.len() > 1 { + is_asc = self.port_sys.ports[0] < self.port_sys.ports[self.port_sys.ports.len() - 1]; + } + let mut left = 0; + let mut right = self.port_sys.ports.len(); + + while left < right { + let mid = left + (right - left) / 2; + + if is_asc { + match pname.cmp(&self.port_sys.ports[mid].name) { + Ordering::Less => right = mid, + Ordering::Equal => return Some(self.port_sys.ports[mid].clone()), + Ordering::Greater => left = mid + 1, + } + } else { + match pname.cmp(&self.port_sys.ports[mid].name) { + Ordering::Less => left = mid + 1, + Ordering::Equal => return Some(self.port_sys.ports[mid].clone()), + Ordering::Greater => right = mid, + } + } + } + None + } +} + +impl TomlConfig for Metadata {} + +impl Default for Metadata { + fn default() -> Self { + Self { + port_sys: PortSys { + categories: vec!["base".to_string(), "general".to_string()], + ports: vec![Ports { + name: "general/test_prt".to_string(), + version: "1.0".to_string(), + modified: "00000".to_string(), + }], + }, + system: System { + releases: vec!["v2.0".to_string()], + }, + } + } +} diff --git a/libcport/src/port.rs b/libcport/src/port.rs new file mode 100644 index 0000000000000000000000000000000000000000..c4c9780f75222d541f92cb71f08acf16f56a413d --- /dev/null +++ b/libcport/src/port.rs @@ -0,0 +1,601 @@ +//! Contains a `Port` type to abstract from the features of the port system. Allows to +//! create ports, delete ports (from the port system, not the OS), check them for +//! correctness and get information about ports. +//! +//! ## Examples +//! +//! ``` +//! use libcport::port::Port; +//! use libcport::port::PortConfig; +//! +//! let port = Port::new("general/test_prt"); +//! // "general/test_prt" - is the port name adopted in the port system, which is for +//! // testing purposes only. The `general/test_prt` port does not contain any working +//! // build instructions that can in any way change the operating system installed on +//! // your computer. You can use the `libcport::conts::TEST_PNAME` constant to get +//! // access to the `general/test_prt` port. +//! +//! // check DIRECTORY (`libcport::consts::PORTS_DIR/PORT_NAME`) exists... +//! let is_exists_dir: bool = port.dir_exists(); +//! +//! // Get a list of the port files to be installed on the system +//! // let flist = port.get_flist().unwrap().collect(); +//! +//! // Gets information about the port - the contents of the `port.toml` port file +//! let prt_info = PortConfig::new(&port).unwrap(); +//! ``` + +use anyhow::Result; +use serde::Deserialize; +use serde::Serialize; + +use std::fs::File; +use std::io; +use std::io::BufRead; +use std::path::Path; + +use crate::consts::PORTS_DIR; +use crate::errors::CportError; +use crate::traits::TomlConfig; + +/// Fundamental data type for working with ports +#[derive(Debug)] +pub struct Port { + /// Port name + /// + /// ## Example: + /// `general/test_prt` + pub name: String, + + /// Path to the ports system directory in the file system + /// + /// ## Example: + /// `/usr/ports` + pub root_pth: String, + + /// Path to the port in the file system + /// + /// ## Example: + /// `/usr/ports/general/test_prt` + pub port_pth: String, +} + +/// The contents of the `port.toml` port configuration file +/// +/// ## File example +/// ```no-test +/// [package] +/// name = "acl" +/// version = "2.3.1" +/// description = "Base pkg" +/// maintainers = ["Michail Krasnov "] +/// releases = [ "v2.0a1", "v2.0a2", "v2.0a3",] +/// priority = "system" +/// usage = 6.1 +/// upgrade_mode = "soft" +/// build_time = 0.1 +/// +/// [deps] +/// required = [ +/// "base/attr", +/// "base/bash", +/// "base/binutils", +/// "base/coreutils", +/// "base/gcc", +/// "base/gettext", +/// "base/grep", +/// "base/m4", +/// "base/make", +/// "base/perl", +/// "base/sed", +/// "base/texinfo" +/// ] +/// +/// [port] +/// url = "https://gitlab.com/calmiralinux/cabs/blocked-packages/-/raw/main/acl-2.3.1.tar.xz" +/// file = "acl-2.3.1.tar.xz" +/// md5 = "95ce715fe09acca7c12d3306d0f076b2" +/// sha256 = "c0234042e17f11306c23c038b08e5e070edb7be44bef6697fb8734dcff1c66b1" +/// ``` +/// +/// ## Usage example +/// ``` +/// use libcport::port::Port; +/// use libcport::port::PortConfig; +/// +/// let port = Port::new("general/test_prt"); +/// let pconf = PortConfig::new(&port).unwrap(); +/// assert_eq!(&pconf.package.name, "vagina"); +/// ``` +#[derive(Debug, Deserialize, Serialize, PartialEq, Clone)] +pub struct PortConfig { + /// Contains the parameters of the package to be ported + pub package: PackageConfigSection, + + /// Describes package dependencies + pub deps: Option, + + /// Information for `cport`: download URLs and checksums + pub port: Option, +} + +/// Information about the ported package. +/// +/// The data from here is intended for human use. Some of the +/// parameters can also be used inside `cport`. +#[derive(Debug, Deserialize, Serialize, PartialEq, Default, Clone)] +pub struct PackageConfigSection { + /// Package name + /// + /// ## Example + /// - **Port name:** `base/cport` + /// - **Package name:** `cport` + pub name: String, + + /// Package version + /// + /// ## Example + /// `v2.0a3:1` + pub version: String, + + /// Short description of the package + /// + /// The package description in theory can be any length, + /// but in practice does not exceed 80 characters. + pub description: String, + + /// List of creators and/or maintainers of this *port* + pub maintainers: Vec, + + /// Port priority (`system`/`user`) + /// + /// System ports (`package.priority` = `system`) cannot be + /// removed from the system by `cport` means. The `cport` + /// can do anything with user ports (`package.priority` = + /// `user`). + pub priority: String, + + /// Disk space required by the *installed* port + /// + /// Space is metered in megabytes + pub usage: f64, + + /// Port upgrade mode + /// + /// `soft` means installing a new version "on top" of + /// an old one. In `hard` mode, the old version of the + /// port is completely uninstalled and then the new + /// version is installed. + pub upgrade_mode: String, + + /// Relative time to build a port from source code + pub build_time: f64, +} + +/// The structure defines a list of port dependencies +#[derive(Debug, Deserialize, Serialize, PartialEq, Default, Clone)] +pub struct DepsConfigSection { + /// Required dependencies + pub required: Option>, + + /// Recommend dependencies + pub recommend: Option>, + + /// Optional dependencies + pub optional: Option>, +} + +/// Port information for use within `cport` only: links for downloading +/// archives and other data +#[derive(Debug, Deserialize, Serialize, PartialEq, Default, Clone)] +pub struct PortConfigSection { + /// Link to download archive with port source code + /// + /// > **Warning:** if you need to download more than one archive, + /// > specify one of them in this parameter, and download all the + /// > others using the `base/wget` port. Instructions for downloading + /// > the other archives can be placed in the `prepare()` function of + /// > the `install` file of the port. + pub url: Option, + + /// Downloaded file name + /// + /// > **Warning:** name of the downloaded file derives `cport` from + /// > the `url` parameter. Use the `file` parameter only when it is + /// > impossible to correctly determine the name of the downloaded + /// > file from its URL. + pub file: Option, + + /// MD5 check sum to check if the archive was downloaded correctly + pub md5: Option, + + /// SHA256 check sum to check if the archive was downloaded correctly + pub sha256: Option, +} + +#[derive(Debug)] +pub enum Warnings<'a> { + /// Use of a large number of unnecessary port dependencies. + /// + /// A warning is shown when any parameter in the `deps` section + /// contains dependencies from the `base/` category. + /// + /// ## Value: + /// + /// 1. String from parameter containing unnecessary ports. + /// 2. Dependency type + OverDependencies(&'a str, &'a str), + + /// Very long port description. + /// + /// A warning is shown when the port description is more than + /// 80 characters long. + /// + /// ## Value: + /// + /// Description text. + LongDescription(&'a str), + + /// Using unnecessary `port.file` parameter. + /// + /// The `port.file` parameter is unnecessary if it is not equal + /// to `Option::None` and the file name can be calculated from + /// the `port.url` parameter. + /// + /// It is considered impossible to compute the file name from + /// the `port.url` parameter in the following cases: + /// + /// 1. When there is no mention of the file in the parameter (URL). + /// 2. When the URL leads to GitHub, GitLab or Sourceforge. + /// + /// ## Value: + /// + /// 1. URL from the `port.url` parameter. + /// 2. The name of the file extracted from the URL. + UnnecessaryParameter(&'a str, &'a str), +} + +impl Default for PortConfig { + fn default() -> Self { + Self { + package: PackageConfigSection { + priority: "user".to_string(), + upgrade_mode: "soft".to_string(), + ..Default::default() + }, + deps: Some(DepsConfigSection::default()), + port: Some(PortConfigSection::default()), + } + } +} + +impl Port { + /// Creates a new instance of `Port` + /// + /// ## Example + /// ``` + /// use libcport::port::Port; + /// let port = Port::new("general/test_prt"); + /// ``` + pub fn new(pname: &str) -> Self { + Self { + name: pname.to_string(), + root_pth: PORTS_DIR.to_string(), + port_pth: format!("{PORTS_DIR}/{pname}"), + } + } + + /// Creates a new instance of `Result` + /// + /// > **Warning:** this method does not check if the required port files + /// > (`install` and `port.toml`) are correct. + pub fn try_new(pname: &str) -> Result { + let port_pth = format!("{PORTS_DIR}/{pname}"); + if !Path::new(&port_pth).is_dir() { + return Err(CportError::PortNotFound(pname.to_string())); + } + + let files = ["install", "port.toml"]; + for file in files { + let file_pth = Path::new(&port_pth).join(file); + if !file_pth.is_file() { + return Err(CportError::PortFileNotFound( + file.to_string(), + pname.to_string(), + )); + } + } + + Ok(Self { + name: pname.to_string(), + root_pth: PORTS_DIR.to_string(), + port_pth, + }) + } + + /// Sets a new path to the ports system directory + /// + /// - Default path: `libcport::consts::PORTS_DIR` + /// - Custom path: `pth` (argument) + pub fn set_path>(&self, pth: P) -> Self { + let pth = pth.as_ref().to_str().unwrap_or("/usr/ports"); + Self { + name: self.name.clone(), + root_pth: pth.to_string(), + port_pth: format!("{pth}/{}", &self.name), + } + } + + /// Separates the port name into its individual + /// components (category, subcategory, name) + /// + /// The port name is a single string (`std::string::String`) + /// that contains the category name, subcategory name + /// (*optional*) and package name. All these names + /// are separated by `/`. + /// + /// ## Example + /// ``` + /// use libcport::port::Port; + /// + /// let port = Port::new("general/test_prt"); + /// let port_comps = port.split_name(); + /// + /// assert_eq!(port_comps, vec!["general", "test_prt"]); + /// ``` + pub fn split_name(&self) -> Vec<&str> { + self.name.split('/').collect() + } + + /// Checks if there is a directory with the port + /// + /// Returns `true` if and only if there is a port directory in the ports system and + /// this directory contains at least one required file (`install`, `port.toml`). + /// + /// ## Example: + /// ``` + /// use libcport::port::Port; + /// let port = Port::new("general/test_prt"); + /// let other_port = Port::new("general/aaabbbccc"); + /// + /// assert!(port.dir_exists()); + /// assert!(!other_port.dir_exists()); + /// ``` + pub fn dir_exists(&self) -> bool { + let ppth = Path::new(&self.port_pth); + if !ppth.is_dir() { + return false; + } + + let flist = ["install", "port.toml"]; + let mut rslt = false; + + for file in flist { + let pth = Path::new(&self.port_pth).join(file); + if pth.exists() { + rslt = true; + } + } + + rslt + } + + /// Gets a list of files installed on the system + pub fn get_flist(&self) -> Result>> { + let f = File::open(format!("{}/files.list", &self.port_pth))?; + let buf_r = io::BufReader::new(f); + let lines = buf_r.lines(); + Ok(lines) + } +} + +impl PortConfig { + /// Gets the name of the downloaded archive with the port source code + /// + /// ## Examples: + /// - 1. `port.toml` + /// ```no-test + /// ... + /// [port] + /// url = "https://ftp.gnu.org/gnu/glibc/glibc-2.36.tar.xz" + /// ``` + /// - 1. Returned value: `glibc-2.36.tar.xz` + /// + /// - 2. `port.toml` + /// ```no-test + /// ... + /// [port] + /// url = "https://ftp.gnu.org/gnu/glibc/glibc-2.36.tar.xz" + /// file = "GlIbC-2.36.txz" + /// ``` + /// - 2. Returned value: `GlIbC-2.36.txz` + /// + /// - 3. `port.toml` + /// ```no-test + /// ... + /// [port] + /// ``` + /// - 3. Returned value: None + pub fn get_downloaded_fname(&self) -> Option { + if let Some(port_section) = &self.port { + // Проверка наличия параметра `port.file`. + // Этот параметр является опциональным и сохранён только для + // совместимости со старыми портами или для тех портов, для + // которых невозможно определить имя скачанного файла. + if port_section.file.is_some() { + port_section.file.clone() + } else { + // В случае отсутствия параметра `port.file` используем + // параметр `port.url` - имя файла достаём из его + // последнего компонента URL + if let Some(url) = &port_section.url { + let (_, fname) = url.rsplit_once('/').unwrap_or(("", "tmp.bin")); + Some(fname.to_string()) + } else { + None + } + } + } else { + None + } + } + + /// Generates the contents of the port description Markdown + /// file based on the config data + pub fn get_markdown_string(&self) -> String { + let mut md = String::new(); + md.push_str(&format!( + "# {}-{}\n\n", + &self.package.name, &self.package.version, + )); + + md.push_str(&format!("> {}\n\n", &self.package.description)); + md.push_str("## Detailed information\n"); + md.push_str("### Port/package\n\n"); + md.push_str("- **maintainers:**"); + + for maintainer in &self.package.maintainers { + md.push_str(&format!(" {}", maintainer)); + } + + md.push('\n'); + md.push_str(&format!("- **usage:** {} Mb\n\n", self.package.usage)); + + if self.deps.is_some() { + md.push_str("### Dependencies\n\n"); + let deps = &self.deps.clone().unwrap_or_default(); + + if deps.required.is_some() { + // Panic not expected + md.push_str("- **required:**"); + for dependency in deps.required.as_ref().unwrap() { + md.push_str(&format!(" {}", dependency)); + } + } + md.push('\n'); + + if deps.recommend.is_some() { + // Panic not expected + md.push_str("- **recommend:**"); + for dependency in deps.recommend.as_ref().unwrap() { + md.push_str(&format!(" {}", dependency)); + } + } + md.push('\n'); + + if deps.optional.is_some() { + // Panic not expected + md.push_str("- **optional:**"); + for dependency in deps.optional.as_ref().unwrap() { + md.push_str(&format!(" {}", dependency)); + } + } + md.push('\n'); + } + md.push('\n'); + + md + } +} + +impl TomlConfig for PortConfig {} + +impl<'a> Warnings<'a> { + pub fn new(pconf: &'a PortConfig) -> Vec { + let mut result = Vec::new(); + if let Some(descr) = Self::check_descriprion(&pconf.package.description) { + result.push(descr); + } + if let Some(deps) = &pconf.deps { + for dep in Self::check_deps_all(deps) { + result.push(dep); + } + } + result + } + + fn check_deps_all(deps: &'a DepsConfigSection) -> Vec { + let mut warnings = Vec::new(); + if let Some(required) = &deps.required { + warnings.append(&mut Self::check_deps(required, "required")); + } + if let Some(recommend) = &deps.recommend { + warnings.append(&mut Self::check_deps(recommend, "recommend")); + } + if let Some(optional) = &deps.optional { + warnings.append(&mut Self::check_deps(optional, "optional")); + } + warnings + } + + fn check_deps(deps: &'a [String], dep_type: &'a str) -> Vec { + let mut warnings = Vec::new(); + for dep in deps { + if dep.contains("base/") { + warnings.push(Self::OverDependencies(dep, dep_type)); + } + } + warnings + } + + fn check_descriprion(description: &'a str) -> Option { + if description.len() > 80 { + Some(Self::LongDescription(description)) + } else { + None + } + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::consts::TEST_PNAME; + + #[test] + fn port_base_test() { + let port = Port::new("general/test_prt"); + assert_eq!(port.name, TEST_PNAME.to_string()); + assert_eq!(port.root_pth, PORTS_DIR.to_string()); + assert_eq!(port.port_pth, format!("{PORTS_DIR}/{TEST_PNAME}")); + } + + #[test] + fn check_dir_exists_test() { + let port = Port::new(TEST_PNAME); + assert!(port.dir_exists()); + } + + #[test] + fn split_name_test() { + let port = Port::new(TEST_PNAME); + assert_eq!(port.split_name(), vec!["general", "test_prt"]); + } + + #[test] + fn get_ptoml_test() { + let port = Port::new(TEST_PNAME); + let pconf = PortConfig::from_port(&port).unwrap(); + + let pconf_new = PortConfig { + package: PackageConfigSection { + name: "vagina".to_string(), + version: "0.1".to_string(), + description: "Vagina.".to_string(), + maintainers: vec!["vaginal worm".to_string()], + priority: "system".to_string(), + usage: 666.0, + upgrade_mode: "hard".to_string(), + build_time: 666.0, + }, + deps: Some(DepsConfigSection { + required: Some(vec!["base/bash".to_string()]), + recommend: Some(vec!["base/bash".to_string()]), + optional: Some(vec!["base/bash".to_string()]), + }), + port: None, + }; + + assert_eq!(pconf, pconf_new); + } +} diff --git a/libcport/src/remove.rs b/libcport/src/remove.rs new file mode 100644 index 0000000000000000000000000000000000000000..9489e3cb6f812407d1d0cec36d9509fea9bfc245 --- /dev/null +++ b/libcport/src/remove.rs @@ -0,0 +1,53 @@ +//! Functions for deleting port files from the system + +use crate::port::Port; +use anyhow::Result; +use libmsg::Message; +use std::fs; +use std::path::Path; + +/// Contains methods for deleting installed port files +/// +/// ## Examples +/// ```ignore +/// use libcport::remove::Remove; +/// use libcport::port::Port; +/// +/// let port = Port::new("base/acl"); +/// let remove = Remove::new( +/// &port, /* port name */ +/// "/opt/program", /* installation prefix */ +/// ); +/// remove.remove_files().unwrap(); +/// ``` +pub struct Remove<'a> { + port: &'a Port, + prefix: &'a str, +} + +impl<'a> Remove<'a> { + pub fn new(port: &'a Port, prefix: &'a str) -> Self { + Self { port, prefix } + } + + /// Deletes files and links to port files that are found in the system + pub fn remove_files(&self, verbose: bool) -> Result<()> { + let files = self.port.get_flist()?; + + for file in files + .map_while(|f| f.ok()) + .map(|s| format!("{}/{}", self.prefix, s)) + { + let file = Path::new(&file); + if file.is_file() || file.is_symlink() { + fs::remove_file(file)?; + if verbose { + let msg = Message::new(2, "remove"); + msg.msg2(format!("removed file '{}'", file.display())); + } + } + } + + Ok(()) + } +} diff --git a/libcport/src/traits.rs b/libcport/src/traits.rs new file mode 100644 index 0000000000000000000000000000000000000000..9cfdfcd8c911d48accf4de8787cf41aad512b89d --- /dev/null +++ b/libcport/src/traits.rs @@ -0,0 +1,75 @@ +use crate::errors::CportError; +use crate::port::Port; +use serde::Deserialize; +use serde::Serialize; +use std::fs; +use std::fs::File; +use std::io::Write; +use std::path::Path; + +pub trait TomlConfig { + /// Reads and parses the TOML configuration file + fn parse>(path: P) -> Result + where + Self: std::marker::Sized, + for<'de> Self: Deserialize<'de>, + { + let conf_str = + fs::read_to_string(path).map_err(|err| CportError::ReadFile(err.to_string()))?; + let data: Self = + toml::from_str(&conf_str).map_err(|err| CportError::Deserialize(err.to_string()))?; + Ok(data) + } + + /// Parses the TOML string + fn from_str(contents: &str) -> Result + where + Self: std::marker::Sized, + for<'de> Self: Deserialize<'de>, + { + let data: Self = + toml::from_str(contents).map_err(|err| CportError::Deserialize(err.to_string()))?; + Ok(data) + } + + /// Parses the port configuration file (`port.toml`) + fn from_port(port: &Port) -> Result + where + Self: std::marker::Sized, + for<'de> Self: Deserialize<'de>, + { + let ppth = Path::new(&port.port_pth).join("port.toml"); + Self::parse(ppth) + } + + fn write>(&self, path: P) -> Result<(), CportError> + where + Self: std::marker::Sized, + for<'de> Self: serde::Deserialize<'de> + Serialize, + { + let conf_str = self.to_string()?; + + if path.as_ref().exists() { + std::fs::remove_file(&path) + .map_err(|err| CportError::RemoveFileDir(err.to_string()))?; + } + + let mut f = File::options() + .create_new(true) + .write(true) + .open(path) + .map_err(|err| CportError::ReadFile(err.to_string()))?; + f.write(conf_str.as_bytes()) + .map_err(|err| CportError::CreateFile(err.to_string()))?; + + Ok(()) + } + + fn to_string(&self) -> Result + where + Self: std::marker::Sized, + Self: Serialize, + { + toml::to_string(&self).map_err(|err| CportError::Serialize(err.to_string())) + } +} diff --git a/libcport/src/utils.rs b/libcport/src/utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..a229a9dfc2d336e5a704793e5750e0d45db25939 --- /dev/null +++ b/libcport/src/utils.rs @@ -0,0 +1,59 @@ +use anyhow::Result; +use std::cmp::min; +use std::fs::File; +use std::io::Write; +use std::path::Path; + +use futures_util::StreamExt; +use indicatif::ProgressBar; +use indicatif::ProgressStyle; +use reqwest; +use tokio; + +pub fn pth_to_str(pth: &Path) -> String { + pth.to_str().unwrap_or("").to_string() +} + +/// Скачивает архив с исходным кодом порта +#[tokio::main] +pub async fn download(url: &str, path: &str, fname: &Option) -> Result<()> { + let fname = match fname { + Some(f) => f, + None => { + let (_, file) = url.rsplit_once('/').unwrap_or(("", "tmp.bin")); + file + } + }; + let file_name = Path::new(path).join(fname); + + let res = reqwest::get(url).await?; + let total_size = res.content_length().unwrap_or(0); + + println!("Length: {total_size}"); + println!("Downloaded to: «{}/{}»", path, fname); + + let pb = ProgressBar::new(total_size); + pb.set_style( + ProgressStyle::default_bar() + .template("{msg} [{bar}] {bytes}/{total_bytes} {elapsed}/{eta}")? + .progress_chars("=> "), + ); + pb.set_message(format!("Download «{fname}»")); + + let mut dest = File::create(&file_name)?; + + let mut downloaded: u64 = 0; + let mut stream = res.bytes_stream(); + + while let Some(item) = stream.next().await { + let chunk = item?; + dest.write_all(&chunk)?; + let new = min(downloaded + (chunk.len() as u64), total_size); + downloaded = new; + pb.set_position(new); + } + + pb.finish(); + + Ok(()) +} diff --git a/libcport/tests/test.db b/libcport/tests/test.db new file mode 100644 index 0000000000000000000000000000000000000000..3c9ba3ed0abb9a99f897d402ee0abb2434927036 Binary files /dev/null and b/libcport/tests/test.db differ diff --git a/libportutil/Cargo.toml b/libportutil/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..302a6dfafdad5f2d4a3d7fdbce0e45603f80462b --- /dev/null +++ b/libportutil/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "libportutil" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +thiserror.workspace = true +serde.workspace = true +ron.workspace = true +toml.workspace = true +tokio.workspace = true +sha256.workspace = true +md5.workspace = true diff --git a/libportutil/src/archive.rs b/libportutil/src/archive.rs new file mode 100644 index 0000000000000000000000000000000000000000..57ab276e2c5cfbcac6105ef5485e1c2f183a2897 --- /dev/null +++ b/libportutil/src/archive.rs @@ -0,0 +1,140 @@ +use std::path::{Path, PathBuf}; + +use serde::{Deserialize, Serialize}; +use sha256::try_digest; +use tokio::fs; + +use crate::{ + errors::PortUtilError, execute, get_pkg_root_dir, get_port_name, get_work_dir, split_last, +}; + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct Archive { + pub port_work_dir: String, + pub port_root_dir: String, + pub url: String, + pub port_full_name: String, + pub file_name: String, + pub package_name: String, + pub md5: String, + pub sha256: String, +} + +impl Archive { + /// Create the new instance. + /// ## Arguments + /// * 'url` an url to download archive + /// * 'package_name` an optional name of a package. Will be detected from an 'url' if not provided. + pub fn new(url: &str, port_full_name: &str) -> Result { + let (_, file_name) = split_last(url, "/")?; + let package_name = get_port_name(port_full_name)?; + + Ok(Self { + port_work_dir: get_work_dir(&package_name)?, + port_root_dir: get_pkg_root_dir(&package_name)?, + url: url.to_string(), + port_full_name: port_full_name.to_string(), + file_name, + package_name, + ..Default::default() + }) + } + + /// Deserialize given string + pub fn from_ron(ron: &str) -> Result { + ron::from_str(ron).map_err(|err| PortUtilError::Deserialize(err.to_string())) + } + + /// Serialize self into 'ron'. + pub fn to_ron(&self) -> Result { + ron::to_string(&self).map_err(|err| PortUtilError::Serialize(err.to_string())) + } + + /// Calculate md5 and sha256 hash + pub async fn calculate_hashes(&mut self) -> Result<(), PortUtilError> { + self.md5 = Self::md5(&format!("{}/{}", self.port_work_dir, self.file_name)).await?; + self.sha256 = Self::sha256(&format!("{}/{}", self.port_work_dir, self.file_name)).await?; + + Ok(()) + } + + /// Download a file from the server by calling the `wget` command + pub async fn download(&self) -> Result<(), PortUtilError> { + let _output = execute::execute( + "wget", + vec![ + &self.url, + &format!("--directory-prefix={}/", self.port_work_dir), + ], + Some(Path::new(&self.port_work_dir).join("download.log")), + )?; + + Ok(()) + } + + /// Extract the downloaded archive + pub async fn extract(&self) -> Result<(), PortUtilError> { + let archive_path = format!("{}/{}", self.port_work_dir, self.file_name); + + let (cmd, option) = if Path::new(&self.file_name).extension().unwrap_or_default() == "zip" { + ( + "unzip", + vec![ + "-ou".to_string(), + "-d".to_string(), + self.port_work_dir.clone(), + archive_path, + ], + ) + } else { + ( + "tar", + vec![ + "-xvf".to_string(), + archive_path, + format!("--directory={}/", self.port_work_dir), + ], + ) + }; + + execute::execute( + cmd, + option, + Some(Path::new(&self.port_work_dir).join("extract.log")), + )?; + + Ok(()) + } + + /// Copy the given file + pub async fn copy(&self, file_path: PathBuf) -> Result<(), PortUtilError> { + execute::execute( + "cp", + vec![file_path, PathBuf::from(self.port_work_dir.clone())], + None, + )?; + + Ok(()) + } + + /// Calculate a md5 checksum + async fn md5(path: &str) -> Result { + let file = fs::read(path).await.map_err(|err| { + PortUtilError::IOFile(format!("Failed to read '{}' cause {}", path, err)) + })?; + let digest = md5::compute(file); + + Ok(format!("{:x}", digest)) + } + + /// Calculate a sha256 checksum + async fn sha256(path: &str) -> Result { + let hash = try_digest(path).map_err(|err| { + PortUtilError::IOFile(format!( + "Failed to calculate sha256 from '{}'cause {}", + path, err + )) + })?; + Ok(hash) + } +} diff --git a/libportutil/src/build.rs b/libportutil/src/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..484063a3e7bdb17196688142d75b73b811b66da3 --- /dev/null +++ b/libportutil/src/build.rs @@ -0,0 +1,112 @@ +use std::{collections::HashMap, fs::File, path::PathBuf, process::Command, time::SystemTime}; + +// use tokio::process::Command; + +use crate::errors::PortUtilError; + +pub struct PortBuild { + env: HashMap, + show_output: bool, + time_elapsed: f64, +} + +impl PortBuild { + pub fn new( + work_dir: String, + port_dir: String, + dest_dir: String, + threads: usize, + show_output: bool, + ) -> Self { + let mut env: HashMap = HashMap::new(); + + env.insert("WORK_DIR".to_string(), work_dir); + env.insert("PORT_DIR".to_string(), port_dir); + env.insert("JOBS".to_string(), threads.to_string()); + env.insert( + "CPORT_INSTALLATION_PREFIX".to_string(), + format!("{}/", dest_dir), + ); + + Self { + env, + show_output, + time_elapsed: 0., + } + } + + pub async fn build(&mut self, function: &str) -> Result { + if !PathBuf::from(self.env.get("WORK_DIR").unwrap_or(&String::new())) + .join("install") + .exists() + { + return Err(PortUtilError::IOFile( + "Couldn't find the 'install' file".to_string(), + )); + } + + self.env.insert("RUN".to_string(), function.to_string()); + + self.execute_build().await + } + + async fn execute_build(&mut self) -> Result { + let time = SystemTime::now(); + + let result = if self.show_output { + Command::new("bash") + .args(vec![ + "-c", + &format!( + "{}/port_build.sh", + self.env.get("WORK_DIR").unwrap_or(&String::new()) + ), + ]) + .envs(self.env.clone()) + .status() + .map_err(|err| PortUtilError::Run("build".to_string(), err.to_string()))? + } else { + let log_path = + PathBuf::from(self.env.get("WORK_DIR").unwrap_or(&String::new())).join("build.log"); + let log_file = File::options() + .append(true) + .create(true) + .open(&log_path) + .map_err(|err| { + PortUtilError::IOFile(format!( + "Failed to open '{}' cause {}", + log_path.to_string_lossy(), + err + )) + })?; + + Command::new("bash") + .args(vec![ + "-c", + &format!( + "{}/port_build.sh", + self.env.get("WORK_DIR").unwrap_or(&String::new()) + ), + ]) + .envs(self.env.clone()) + .stdout(log_file.try_clone().map_err(|err| { + PortUtilError::IOFile(format!("Failed to use 'build.log' cause: '{}'", err)) + })?) + .stderr(log_file) + .output() + .map_err(|err| PortUtilError::Run("build".to_string(), err.to_string()))? + .status + }; + + let code = result.code().unwrap_or_default(); + self.time_elapsed += match time.elapsed() { + Ok(elapsed) => elapsed.as_secs_f64(), + Err(_) => 0., + }; + + Ok(code) + } + pub fn build_time(&self) -> f64 { + self.time_elapsed / 189. + } +} diff --git a/libportutil/src/container.rs b/libportutil/src/container.rs new file mode 100644 index 0000000000000000000000000000000000000000..042701d3e0060b314750b4908f4970235b997c7d --- /dev/null +++ b/libportutil/src/container.rs @@ -0,0 +1,11 @@ +use crate::errors::PortUtilError; + +#[allow(dead_code)] +pub fn create() -> Result<(), PortUtilError> { + Ok(()) +} + +#[allow(dead_code)] +pub fn clean() -> Result<(), PortUtilError> { + Ok(()) +} diff --git a/libportutil/src/env.rs b/libportutil/src/env.rs new file mode 100644 index 0000000000000000000000000000000000000000..6db1f77e6757f4e3c69d8de5c8b8ec0f99644bfa --- /dev/null +++ b/libportutil/src/env.rs @@ -0,0 +1,73 @@ +use std::env; + +use crate::errors::PortUtilError; + +/// Return a path of the work directory provided by environment variable 'CALMIRA_WORK' +pub fn get_work_base_dir() -> Result { + get_env_variable("CALMIRA_WORK").map_err(|err| PortUtilError::EnvGet(err.to_string())) +} + +/// Return value of the given environment variable. Error if variable not found. +pub fn get_env_variable(name: &str) -> Result { + Ok(env::var(name) + .map_err(|_| PortUtilError::EnvVar(name.to_string()))? + .trim_end_matches('/') + .to_string()) +} + +/// Return a work directory provided by environment variable 'CALMIRA_PORTS' +pub fn get_ports_base_dir() -> Result { + get_env_variable("CALMIRA_PORTS").map_err(|err| PortUtilError::EnvGet(err.to_string())) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_env_variable_found() { + env::set_var("TEST", "successfull"); + + let result = get_env_variable("TEST").unwrap(); + assert_eq!(result, "successfull".to_string()); + + let result = get_env_variable("NOT_SET"); + assert!(result.is_err()); + } + + #[test] + fn test_get_env_variable_not_found() { + let result = get_env_variable("NOT_SET"); + assert!(result.is_err()); + } + + #[test] + fn test_get_ports_base_dir() { + let expected = "/test/calmira"; + env::set_var("CALMIRA_PORTS", expected); + let result = get_ports_base_dir(); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected.to_string()); + + env::remove_var("CALMIRA_PORTS"); + let result = get_ports_base_dir(); + + assert!(result.is_err()); + } + + #[test] + fn test_get_work_base_dir() { + let expected = "/test/calmira"; + env::set_var("CALMIRA_WORK", expected); + let result = get_work_base_dir(); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected.to_string()); + + env::remove_var("CALMIRA_WORK"); + let result = get_work_base_dir(); + + assert!(result.is_err()); + } +} diff --git a/libportutil/src/errors.rs b/libportutil/src/errors.rs new file mode 100644 index 0000000000000000000000000000000000000000..b48ed10fcf26313efa9af438f6eecafa640b852e --- /dev/null +++ b/libportutil/src/errors.rs @@ -0,0 +1,27 @@ +#[derive(Debug, thiserror::Error)] +pub enum PortUtilError { + #[error("Failed to split string {0}")] + SplitLast(String), + #[error("failed to create directory: {0}")] + IoDir(String), + #[error("{0}")] + IOFile(String), + #[error("command {0} exited with the error {1}")] + Run(String, String), + #[error("failed to serialize Package cause: {0}")] + Serialize(String), + #[error("failed to deserialize given string cause: {0}")] + Deserialize(String), + #[error("environment variable '{0}' not found")] + EnvVar(String), + #[error("{0}. Please set it.")] + EnvGet(String), + #[error("{0}")] + Download(String), + #[error("{0}")] + Progress(String), + #[error("failed to prepare chroot environment")] + ContainerCreate, + #[error("failed to clean up chroot environment")] + ContainerClean, +} diff --git a/libportutil/src/execute.rs b/libportutil/src/execute.rs new file mode 100644 index 0000000000000000000000000000000000000000..861666744b7924002a1d892c2be06ce289335ea5 --- /dev/null +++ b/libportutil/src/execute.rs @@ -0,0 +1,41 @@ +use std::{ffi::OsStr, fs::File, path::PathBuf, process::Command}; + +use crate::PortUtilError; + +pub fn execute( + command: &str, + args: I, + log_path: Option, +) -> Result +where + I: IntoIterator, + S: AsRef, +{ + let result = if let Some(path) = log_path { + let log_file = File::options() + .append(true) + .create(true) + .open(&path) + .map_err(|err| { + PortUtilError::IOFile(format!( + "Failed to open '{}' cause {}", + path.to_string_lossy(), + err + )) + })?; + + Command::new(command) + .args(args) + .stdout(log_file) + .output() + .map_err(|err| PortUtilError::Run(command.to_string(), err.to_string()))? + .status + } else { + Command::new(command) + .args(args) + .status() + .map_err(|err| PortUtilError::Run(command.to_string(), err.to_string()))? + }; + + Ok(result.code().unwrap_or_default()) +} diff --git a/libportutil/src/lib.rs b/libportutil/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..6670a132dc8e23ee68617e86f322589aac2e0455 --- /dev/null +++ b/libportutil/src/lib.rs @@ -0,0 +1,209 @@ +use std::{fs, path::Path}; + +use errors::PortUtilError; + +pub mod archive; +pub mod build; +pub mod container; +pub mod env; +pub mod errors; +pub mod execute; + +/// Search the last delimiter occurence and slpit string into two substrings +/// ## Arguments +/// * `message` a string to split +/// * `delimiter` a delimiter string +/// ## Return +/// * A tuple with substring or error if delimiteer substring not found +pub fn split_last(message: &str, delimiter: &str) -> Result<(String, String), PortUtilError> { + let (left, right) = message + .rsplit_once(delimiter) + .ok_or(PortUtilError::SplitLast(message.to_string()))?; + Ok((left.to_string(), right.to_string())) +} + +/// Return path to the port and create port dir if it not exists +/// ## Arguments +/// * `port_full_name` a name of a port in format `category/name` +/// ## Return +/// * String which contains full path to the port +pub fn get_port_dir(port_full_name: &str) -> Result { + let dir = format!("{}/ports/{port_full_name}", env::get_ports_base_dir()?); + + create_dir(&dir)?; + + Ok(dir) +} + +/// Return port name +/// ## Arguments +/// * `port_full_name` a name of a port in format `category/name` +/// ## Return +/// * String with port name or error if port name not contains a category +pub fn get_port_name(port_full_name: &str) -> Result { + let (_, name) = split_last(port_full_name, "/")?; + + Ok(name) +} + +/// Return port category +/// ## Arguments +/// * `port_full_name` a name of a port in format `category/name` +/// ## Return +/// * String with category +pub fn get_port_category(port_full_name: &str) -> Result { + let (category, _) = split_last(port_full_name, "/")?; + + Ok(category) +} + +/// Return path to the ports workring directory and create it if not exists +/// ## Arguments +/// * `name` a name of a package +/// ## Return +/// * String which contains full path to the working directory +pub fn get_work_dir(name: &str) -> Result { + let work_dir = format!("{}/src", get_pkg_root_dir(name)?); + + create_dir(&work_dir)?; + + Ok(work_dir) +} + +/// Return root directory of the given package +pub fn get_pkg_root_dir(name: &str) -> Result { + Ok(format!("{}/{}", env::get_work_base_dir()?, name)) +} + +/// Create directory if it not exists +pub fn create_dir(dir_name: &str) -> Result<(), PortUtilError> { + if !Path::new(dir_name).exists() { + fs::create_dir_all(dir_name).map_err(|err| PortUtilError::IoDir(err.to_string()))?; + } + + Ok(()) +} + +/// Remove all files and subdisr in the given directory +pub async fn clean_dir(path: &str) -> Result<(), PortUtilError> { + if Path::new(path).is_dir() { + execute::execute("rm", vec!["-rf", path], None)?; + } + + Ok(()) +} + +// pub async fn download_file(url: &str, path: &Path) -> Result<(), PortUtilError> { +// let client = reqwest::Client::new(); +// let res = client +// .get(url) +// .send() +// .await +// .map_err(|_| PortUtilError::IOFile(format!("Failed to GET from '{}'", &url)))?; +// let total_size = res.content_length().ok_or(PortUtilError::IOFile(format!( +// "Failed to get content length from '{}'", +// &url +// )))?; + +// // Indicatif setup +// let pb = ProgressBar::new(total_size); +// pb.set_style(ProgressStyle::default_bar() +// .template("{msg:>20}\n{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})") +// .map_err(|err| PortUtilError::Progress(format!("Progress failed: {}", err.to_string())))? +// .progress_chars("#>-")); +// pb.set_message(format!( +// "{}", +// path.file_name().unwrap_or_default().to_string_lossy() +// )); + +// // download chunks +// let mut file = File::create(path).await.map_err(|err| { +// PortUtilError::IOFile(format!( +// "Failed to create file '{}' cause {}", +// path.to_string_lossy(), +// err.to_string() +// )) +// })?; +// let mut downloaded: u64 = 0; +// let mut stream = res.bytes_stream(); + +// while let Some(item) = stream.next().await { +// let chunk = +// item.map_err(|_| PortUtilError::Download("Error while downloading file".to_string()))?; +// file.write_all(&chunk).await.map_err(|err| { +// PortUtilError::IOFile(format!( +// "Error while writing to file {} cause {}", +// path.to_string_lossy(), +// err.to_string(), +// )) +// })?; +// let new = min(downloaded + (chunk.len() as u64), total_size); +// downloaded = new; +// pb.set_position(new); +// } + +// // pb.finish_with_message(format!("Downloaded {} to {}", url, path.to_string_lossy())); +// Ok(()) +// } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_work_dir() { + let port_name = "bc"; + let work_dir = "/tmp/test"; + let expected = format!("{work_dir}/{port_name}/src"); + + std::env::set_var("CALMIRA_WORK", work_dir); + + let result = get_work_dir(port_name); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_get_port_dir() { + let port_name = "base/bc"; + let base_dir = "/tmp/test"; + let expected = format!("{base_dir}/ports/{port_name}"); + + std::env::set_var("CALMIRA_PORTS", base_dir); + + let result = get_port_dir(port_name); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected); + } + + #[test] + fn test_get_pkg_root_dir() { + let work_dir = "/test/calmira"; + let package_name = "my_package"; + let expected = format!("{}/{}", work_dir, package_name); + std::env::set_var("CALMIRA_WORK", work_dir); + let result = get_pkg_root_dir(package_name); + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), expected.to_string()); + + std::env::remove_var("CALMIRA_WORK"); + let result = get_pkg_root_dir(package_name); + + assert!(result.is_err()); + } + + #[test] + fn test_split_last() { + let message = "foo/bar/buzz"; + let expected_prefix = "foo/bar"; + let expected_suffix = "buzz"; + + let (prefix, suffix) = split_last(message, "/").unwrap(); + + assert_eq!(prefix, expected_prefix); + assert_eq!(suffix, expected_suffix); + } +} diff --git a/mkport/.mkport.conf b/mkport/.mkport.conf new file mode 100644 index 0000000000000000000000000000000000000000..cbbd4e49afa5f4c4f39c9c45b7e67a7a0896e1cc --- /dev/null +++ b/mkport/.mkport.conf @@ -0,0 +1,5 @@ +files_list_generation = false +tests = false +threads = 2 +src_dir = "/tmp/" +installation_dir = "/tmp/new_port" diff --git a/mkport/Cargo.toml b/mkport/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..ff092d4e5ba59d0650308fd58083b356fb4dd8c7 --- /dev/null +++ b/mkport/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "mkport" +version = "1.0.0" +edition = "2021" +description = "Tool to automate the creation of new ports" +authors = ["Michail Krasnov "] +license = "GPL-3.0-or-later" + +[dependencies] +libcport = { version = "2.0.0", path = "../libcport" } +uuid = { version = "1.5.0", features = ["v4", "fast-rng", "macro-diagnostics"] } + +git2.workspace = true +anyhow.workspace = true +clap.workspace = true +colored.workspace = true +cursive.workspace = true +futures.workspace = true +futures-util.workspace = true +indicatif.workspace = true +libmsg.workspace = true +log.workspace = true +log4rs.workspace = true +reqwest.workspace = true +serde.workspace = true +tokio.workspace = true +toml.workspace = true +walkdir.workspace = true +sha256.workspace = true +md5.workspace = true diff --git a/mkport/README.md b/mkport/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ac05d0c4944c4d4bb40a095d5f157cecaa915a04 --- /dev/null +++ b/mkport/README.md @@ -0,0 +1,32 @@ +# mkport + +`mkport` is a tool for automating the creation of new ports. It asks the user for basic information about the port and then generates the contents of the port on its own, based on the received data. + +The program can test the build of a port, test if the port is built correctly, generate a `files.list` file and send changes to the remote port system repository. + +![](assests/main_win.png) + +## Installation + +- **Dependencies:** `rustc`, `cargo`, `bash`, `tar`, `cmsg` (from [libmsg](https://gitlab.com/calmiralinux/libmsg) crate); + +```bash +# From `cport` (project root) directory + +cargo build --release + +sudo cp -v target/release/mkport /usr/bin +sudo mkdir -pv /usr/share/cport +sudo cp -v libcport/src/install.sh /usr/share/cport +sudo cp -v mkport/assests/install.template /usr/share/cport +``` + +## Usage + +```bash +mkport +``` + +## License + +GNU GPLv3.0 or later. diff --git a/mkport/assests/install.template b/mkport/assests/install.template new file mode 100644 index 0000000000000000000000000000000000000000..5d4b2b6e04ff62c522862bec4357a823a0a6d91b --- /dev/null +++ b/mkport/assests/install.template @@ -0,0 +1,48 @@ +#!/bin/bash -e +# Build script for 'TEMPLATE_PACKAGE' package +# Copyright (C) TEMPLATE_YEARS TEMPLATE_MAINTAINER + +############################################################################### +## ENVIRONMENT VARIABLES: +## + +# - 'CPORT_BUILD_THREADS' - число потоков сборки +# - 'CPORT_INSTALLATION_PREFIX' - префикс установки порта +# - 'SRC_DIR' - путь до исходного кода порта +# - 'PORT_DIR' - путь до каталога с файлами порта ('install', 'port.toml', +# etc.) +# - 'IS_TEST' - тестировать ли порт после сборки +# - 'MAKEFLAGS' - указывает число потоков сборки для GNU Make +# - 'NINJAJOBS' - указывает число потоков сборки для ninja +# - 'LOG_FILE' - путь до файла логов cport + + +############################################################################### +## BUILD INSTRUCTIONS: +## + +NAME="TEMPLATE_NAME" +VERSION="TEMPLATE_VERSION" +#ARCHIVE="TEMPLATE_ARCHIVE" # custom package archive name +#BUILD_DIR="" # custom build dir name + +function prepare() { + TEMPLATE_CONFIGURE +} + +function build() { + TEMPLATE_BUILD_SYSTEM +} + +#function test() { +# TEMPLATE_BUILD_SYSTEM test +#} + +#function preinst() {} + +function pkginst() { + TEMPLATE_BUILD_SYSTEM install +} + +#function postinst() {} + diff --git a/mkport/assests/main_win.png b/mkport/assests/main_win.png new file mode 100644 index 0000000000000000000000000000000000000000..190ec510cc28dab98ed480e6756ae7cbc283b8f8 Binary files /dev/null and b/mkport/assests/main_win.png differ diff --git a/mkport/src/bs.rs b/mkport/src/bs.rs new file mode 100644 index 0000000000000000000000000000000000000000..f632bd451e6fbc1873ec3129f21db198ea900eec --- /dev/null +++ b/mkport/src/bs.rs @@ -0,0 +1,79 @@ +use crate::tmp_port::TmpPort; +use libcport::errors::CportError; +use std::fs; + +pub const ASSESTS_DIR: &str = "/usr/share/cport"; + +pub enum BuildSystem { + Make, + Meson, + Cargo, +} + +impl BuildSystem { + pub fn to_str(&self) -> &str { + match self { + Self::Make => "make", + Self::Meson => "meson", + Self::Cargo => "cargo", + } + } + + pub fn get_prepare_func(&self) -> &str { + match self { + Self::Make => "./configure --prefix=/usr", + Self::Meson => "mkdir -pv build\n\tcd build\n\tmeson --prefix=/usr \\\n\t\t--buildtype=release \\\n\t..", + Self::Cargo => "", + } + } +} + +#[derive(Debug)] +pub struct Maintainer { + pub years: String, + pub name: String, +} + +pub fn get_install_content( + bs: BuildSystem, + port: &TmpPort, + maint: &Maintainer, +) -> Result { + let inst_template = format!("{ASSESTS_DIR}/install.template"); + let mut inst_cont = + fs::read_to_string(&inst_template).map_err(|_| CportError::ReadFile(inst_template))?; + + let file_name = if port.port.url.is_empty() { + None // Если нет параметра `url`, то нечего скачивать, соответственно, нечего и распаковывать + } else if port.port.file.is_empty() { + let (_, f) = port + .port + .url + .rsplit_once('/') + .ok_or(CportError::DataMissing( + "port.url: unable to determine download file name".to_string(), + ))?; + Some(f) + } else { + Some(&port.port.file as &str) + }; + + inst_cont = inst_cont + .replace("TEMPLATE_PACKAGE", &port.package.name) + .replace("TEMPLATE_YEARS", &maint.years) + .replace("TEMPLATE_MAINTAINER", &maint.name) + .replace("TEMPLATE_NAME", &port.package.name) + .replace("TEMPLATE_VERSION", &port.package.version) + .replace("TEMPLATE_CONFIGURE", bs.get_prepare_func()) + .replace("TEMPLATE_BUILD_SYSTEM", bs.to_str()); + + // Если нельзя получить имя файла, то имя файла принимается + // как "none". В таком случае скрипт `install.sh` не будет + // предпринимать попытки распаковки архивов с исходниками. + match file_name { + Some(fname) => inst_cont = inst_cont.replace("TEMPLATE_ARCHIVE", fname), + None => inst_cont = inst_cont.replace("TEMPLATE_ARCHIVE", "none"), + } + + Ok(inst_cont) +} diff --git a/mkport/src/build.rs b/mkport/src/build.rs new file mode 100644 index 0000000000000000000000000000000000000000..300d511e284f1012894ea289d56dff739e7b47fa --- /dev/null +++ b/mkport/src/build.rs @@ -0,0 +1,160 @@ +use std::fs::metadata; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use walkdir::WalkDir; + +use crate::tmp_port::TmpPort; +use libcport::errors::CportError; +use libcport::install::BuildResult; +use libcport::install::Install; +use libcport::port::Port; +use libcport::port::PortConfig; +use libcport::traits::TomlConfig; +use libcport::utils; + +#[derive(Debug)] +pub struct Build<'a> { + port: Port, + tmp_port: &'a TmpPort, + + /// Path to the directory in which the port files will be installed + install_pth: &'a str, + + /// Path to the source code directory + src_pth: &'a str, +} + +impl<'a> Build<'a> { + pub fn new(port_name: &'a TmpPort, port_pth: &'a str) -> Self { + Self { + port: Port::new(&format!( + "{}/{}", + &port_name.package.category, &port_name.package.name, + )) + .set_path(port_pth), + tmp_port: port_name, + install_pth: "/tmp/new_port", + src_pth: "/tmp", + } + } + + pub fn set_installation_pth(mut self, pth: &'a str) -> Self { + self.install_pth = pth; + self + } + + pub fn set_src_pth(mut self, pth: &'a str) -> Self { + self.src_pth = pth; + self + } + + pub fn download(&self) -> Result<(), CportError> { + let pconf = &self.tmp_port.port; + if !pconf.url.is_empty() { + let file = if pconf.file.is_empty() { + None + } else { + Some(pconf.file.clone()) + }; + utils::download(&pconf.url, self.src_pth, &file) + .map_err(|err| CportError::DownloadFile(err.to_string()))?; + } + Ok(()) + } + + pub fn build(&self, threads: u8, test: bool) -> Result { + if threads == 0 { + return Err(CportError::Run( + "The number of threads must not be zero".to_string(), + )); + } + + let install = Install::new( + &self.port, + threads, + self.src_pth, + self.install_pth, + test, + true, + ); + install + .run_inst_script() + .map_err(|err| CportError::Run(err.to_string())) + } + + pub fn gen_flist(&self) -> Result { + let flist = Path::new(&self.port.port_pth).join("files.list"); + if flist.exists() { + std::fs::remove_file(&flist).map_err(|err| { + CportError::IO( + "remove".to_string(), + utils::pth_to_str(&flist), + err.to_string(), + ) + })?; + } + + let mut f = File::options() + .append(true) + .create(true) + .open(&flist) + .map_err(|err| { + CportError::IO( + "open".to_string(), + utils::pth_to_str(&flist), + err.to_string(), + ) + })?; + + let mut length: f64 = 0.; + for entry in WalkDir::new(self.install_pth) + .into_iter() + .filter_map(|f| f.ok()) + { + let pth = entry.path(); + let file = utils::pth_to_str(pth); + if pth.is_file() { + let md = metadata(pth).map_err(|err| { + CportError::IO("read".to_string(), file.clone(), err.to_string()) + })?; + length += md.len() as f64 / 1024. / 1024.; + + let (_, file) = file.rsplit_once(self.install_pth).unwrap_or(("", "")); + + let file = format!("{file}\n"); + + f.write_all(file.as_bytes()).map_err(|err| { + CportError::IO("write".to_string(), file.to_string(), err.to_string()) + })?; + } + } + Ok(length) + } + + pub fn gen_ptoml(&self, usage: f64, time: f64) -> Result<(), CportError> { + let mut ptoml = self.tmp_port.to_pconf(true)?; + ptoml.package.usage = usage; + ptoml.package.build_time = time; + let ptoml_pth = Path::new(&self.port.port_pth).join("port.toml"); + ptoml.write(ptoml_pth)?; + + Ok(()) + } + + pub fn gen_markdown(&self) -> Result<(), CportError> { + let file = Path::new(&self.port.port_pth).join("README.md"); + let md = PortConfig::from_port(&self.port)?.get_markdown_string(); + std::fs::write(&file, md).map_err(|err| { + CportError::IO( + "write".to_string(), + file.as_os_str() + .to_str() + .unwrap_or("unknown file") + .to_string(), + err.to_string(), + ) + })?; + Ok(()) + } +} diff --git a/mkport/src/conf.rs b/mkport/src/conf.rs new file mode 100644 index 0000000000000000000000000000000000000000..2203d53713477d4ce1cb305c03d730ce15fc2d05 --- /dev/null +++ b/mkport/src/conf.rs @@ -0,0 +1,60 @@ +use libcport::traits::TomlConfig; +use serde::Deserialize; +use serde::Serialize; + +#[derive(Debug, Deserialize, Serialize)] +pub struct GlobalConf { + /// The default git repository branch to be cloned is + pub branch: String, + + /// Where the cloning will be performed + pub repo_root_pth: String, + + /// Should changes be sent to the repository automatically + pub autopush: bool, + + /// Maintainer name + pub maintainer: String, + + /// Maintainer email + pub email: String, + + /// Editor program + pub editor: Option, +} + +impl TomlConfig for GlobalConf {} +impl Default for GlobalConf { + fn default() -> Self { + Self { + branch: "testing".to_string(), + repo_root_pth: "/tmp".to_string(), + autopush: false, + maintainer: "Unknown maintainer".to_string(), + email: "unknown@unknown.com".to_string(), + editor: None, + } + } +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct LocalPortConf { + pub files_list_generation: bool, + pub tests: bool, + pub threads: u8, + pub src_dir: String, + pub installation_dir: String, +} + +impl TomlConfig for LocalPortConf {} +impl Default for LocalPortConf { + fn default() -> Self { + Self { + files_list_generation: true, + tests: false, + threads: 1, + src_dir: "/tmp/".to_string(), + installation_dir: "/tmp/new_port".to_string(), + } + } +} diff --git a/mkport/src/git.rs b/mkport/src/git.rs new file mode 100644 index 0000000000000000000000000000000000000000..4f72b1ca50f84e8df4ace9cfdf49472bcfba74b3 --- /dev/null +++ b/mkport/src/git.rs @@ -0,0 +1,441 @@ +//! A module for working with the `git` repository. Uses the `/usr/bin/git` program. + +use std::env; +use std::fs; +use std::path::Path; +use std::process::Command; + +use git2::Repository; +use libcport::errors::CportError; +use uuid::Uuid; + +use crate::tmp_port::TmpPort; + +static GIT: &str = "/usr/bin/git"; + +/// Create, clone, and perform other actions on `git` repositories +/// +/// This version uses the `git2` crate. +/// +/// > **Warning:** It does not work correctly: +/// > +/// > 1. Files are not added to commit. +/// > 2. To send changes to GitLab (analogous to `git push`) use +/// `/usr/bin/git` process instead of `git2`. +pub struct Git2<'a> { + url: &'a str, + branch: &'a str, + current_dir: String, + repo_root_pth: &'a Path, + repo: Repository, +} + +/// Create, clone, and perform other actions on `git` repositories +/// +/// > **Note:** Until `Git2` is stabilized, it is strongly recommended +/// > to use `Git`. +/// +/// This version uses the `/usr/bin/git` (`mkport::git::GIT`) process +/// startup. +pub struct Git<'a> { + /// URL of the repository with the ports that will be cloned. + url: &'a str, + + /// Name of the default cloned branch. + default_branch: &'a str, + + /// Name of the created branch + pub created_branch: Option, + + /// The current directory from which the `mkport` process was started + /// or the `Git::new()` method was executed (value of the `PWD` + /// environment variable). + current_dir: String, + + /// The parent directory for the directory with the cloned repository. + repo_root_prefix: &'a str, + + /// The full path to the directory with the cloned repository. + pub repo_root_pth: String, +} + +impl<'a> Git2<'a> { + /// Clones a remote git-repository and creates a new instance of `Git` + /// + /// - `url` - URL of the repository with the ports that will be cloned; + /// - `branch` - name of the branch to be created; + /// - `repo_root` - where the port system will be cloned. + /// + /// ## Example + /// ``` + /// use mkport::git::Git; + /// let git = Git::new( + /// "https://gitlab.com/calmiralinux/cabs/Ports", + /// "testing", + /// "/tmp", + /// ); + /// ``` + pub fn new(url: &'a str, branch: &'a str, repo_root: &'a str) -> Result { + if Path::new(repo_root).exists() { + return Err(CportError::CreateDir(format!( + "The '{}' directory already exists on the file system", + repo_root, + ))); + } + Ok(Self { + url, + branch, + current_dir: env::var("PWD").unwrap_or("/tmp".to_string()), + repo_root_pth: Path::new(repo_root), + repo: Repository::clone(url, repo_root) + .map_err(|err| CportError::GitError(err.to_string()))?, + }) + } + + pub fn new_open(url: &'a str, branch: &'a str, repo_root: &'a str) -> Result { + Ok(Self { + url, + branch, + current_dir: env::var("PWD").unwrap_or("/tmp".to_string()), + repo_root_pth: Path::new(repo_root), + repo: Repository::open(repo_root) + .map_err(|err| CportError::GitError(err.to_string()))?, + }) + } + + /// Clones a remote git repository to the specified path + pub fn repo_clone(&self) -> Result<(), CportError> { + if self.repo_root_pth.exists() { + fs::remove_dir_all(self.repo_root_pth) + .map_err(|err| CportError::RemoveFileDir(err.to_string()))?; + } + Repository::clone(self.url, self.repo_root_pth) + .map_err(|err| CportError::GitError(err.to_string()))?; + + Ok(()) + } + + pub fn create_branch(&self) -> Result<(), CportError> { + let head = self + .repo + .head() + .map_err(|err| CportError::GitError(err.to_string()))?; + let oid = head.target().unwrap(); + let commit = self + .repo + .find_commit(oid) + .map_err(|err| CportError::GitError(err.to_string()))?; + + self.repo + .branch(self.branch, &commit, false) + .map_err(|err| CportError::GitError(err.to_string()))?; + + let brnch = format!("refs/heads/{}", self.branch); + + let obj = self + .repo + .revparse_single(&brnch) + .map_err(|err| CportError::GitError(err.to_string()))?; + + self.repo + .checkout_tree(&obj, None) + .map_err(|err| CportError::GitError(err.to_string()))?; + self.repo + .set_head(&brnch) + .map_err(|err| CportError::GitError(err.to_string()))?; + + Ok(()) + } + + pub fn create_commit( + &self, + name: &'a str, + email: &'a str, + msg: &'a str, + path: &Path, + ) -> Result<(), CportError> { + if !self.repo_root_pth.join(path).exists() { + return Err(CportError::ReadFile(format!( + "The specified file '{}' does not exist in the file system", + path.to_str().unwrap(), + ))); + } + + let sign = git2::Signature::now(name, email) + .map_err(|err| CportError::GitError(err.to_string()))?; + + let head = self + .repo + .head() + .map_err(|err| CportError::GitError(err.to_string()))?; + let oid = head.target().unwrap(); + + let mut index = self + .repo + .index() + .map_err(|err| CportError::GitError(err.to_string()))?; + + let tree = self + .repo + .find_tree(index.write_tree().unwrap()) + .map_err(|err| CportError::GitError(err.to_string()))?; + + // index.add_path(path) + // .map_err(|err| CportError::GitError(err.to_string()))?; + index + .add_all(["*"].iter(), git2::IndexAddOption::DEFAULT, None) + .unwrap(); + index.write().unwrap(); + self.repo + .set_index(&mut index) + .map_err(|err| CportError::GitError(err.to_string()))?; + + self.repo + .commit( + Some("HEAD"), + &sign, + &sign, + msg, + &tree, + &[&self + .repo + .find_commit(oid) + .map_err(|err| CportError::GitError(err.to_string()))?], + ) + .map_err(|err| CportError::GitError(err.to_string()))?; + + Ok(()) + } + + fn ch_dir>(&self, dir: P) -> Result<(), CportError> { + env::set_current_dir(dir).map_err(|err| CportError::ReadFile(err.to_string())) + } + + pub fn push_changes(&self) -> Result<(), CportError> { + self.ch_dir(self.repo_root_pth)?; + + let command = Command::new("/usr/bin/git") + .arg("push") + .arg("--set-upstream") + .arg("origin") + .arg(self.branch) + .status() + .map_err(|err| CportError::Run(err.to_string()))? + .code() + .unwrap_or(0); + + if command != 0 { + return Err(CportError::RunCode(command)); + } + + self.ch_dir(&self.current_dir)?; + Ok(()) + } +} + +impl<'a> Git<'a> { + /// Creates a new `Git` instance + /// + /// ## Arguments: + /// + /// - `url` - URL of the remote repository; + /// - `branch` - the base branch to be cloned (e.g. `stable`, + /// `testing`, `ports_v2.0`, etc.); + /// - `repo_root` - path to the directory where the subdirectory + /// with the repository will be created. + /// + /// ## Repository cloning + /// + /// Set the name of the directory where the repository will be + /// located. The name has the following format: + /// + /// `{repo_root}/mkport-{UUID}`, where `{repo_root}` is the + /// value of the `repo_root` argument and `{UUID}` is the unique + /// repository ID. + /// + /// > **Warning:** The `new()` function only sets the parameters + /// necessary for cloning. In order to clone a repository, the + /// `repo_clone()` method must be explicitly executed. + /// + /// ## Example + /// + /// ``` + /// use mkport::git::Git; + /// // WARNING! The `git` variable must be mutable to perform some actions! + /// let git = Git::new( + /// "https://gitlab.com/calmiralinux/cabs/ports", /* Repository URL */ + /// "testing", /* default cloned branch (`git clone -b BRANCH`) */ + /// ".", /* destination dir (`./REPOSITORY_DIRECTORY_NAME`) */ + /// ).unwrap(); + /// ``` + pub fn new(url: &'a str, branch: &'a str, repo_root: &'a str) -> Result { + let repo_root_pth = format!("{repo_root}/mkport-{}", Uuid::new_v4()); + if Path::new(&repo_root_pth).exists() { + return Err(CportError::CreateDir(format!( + "The '{}' directory already exists on the system", + repo_root, + ))); + } + Ok(Self { + url, + repo_root_pth, + repo_root_prefix: repo_root, + default_branch: branch, + created_branch: None, + current_dir: env::var("PWD").unwrap_or("/tmp".to_string()), + }) + } + + fn set_current_dir>(&self, dir: P, op: &str) -> Result<(), CportError> { + env::set_current_dir(&dir).map_err(|err| { + CportError::IO( + op.to_string(), + dir.as_ref() + .to_str() + .unwrap_or("UNKNOWN GIT DIR") + .to_string(), + format!("Failed to set current directory: {err}"), + ) + })?; + Ok(()) + } + + /// Clones the repository to the defined directory. + /// + /// ## Return value + /// + /// Path to the repository. + pub fn repo_clone(&self) -> Result { + if Path::new(&self.repo_root_pth).exists() { + fs::remove_dir_all(&self.repo_root_pth) + .map_err(|err| CportError::RemoveFileDir(err.to_string()))?; + } + + self.set_current_dir(self.repo_root_prefix, "Git::repo_clone")?; + + Command::new(GIT) + .arg("clone") + .arg(self.url) + .arg(&self.repo_root_pth) + .arg("-b") + .arg(self.default_branch) + .status() + .map_err(|err| CportError::GitError(err.to_string()))?; + + // Возвращение в исходную директорию сделано для того, чтобы избежать + // неоднозначности в коде. В противном случае после выполнения + // `repo_clone` придётся учитывать изменившееся положение программы. + self.set_current_dir(&self.current_dir, "Git::repo_clone")?; + Ok(self.repo_root_pth.clone()) + } + + fn check_exists(&self, port: &TmpPort) -> bool { + let pth_str = format!( + "{}/ports/{}/{}", + &self.repo_root_pth, &port.package.category, &port.package.name, + ); + let pth = Path::new(&pth_str); + pth.exists() && pth.is_dir() + } + + /// Creates a branch in the cloned repository and switches to it + /// + /// The branch name has the following format: + /// + /// `mkport/{STATUS}/{CATEGORY}-{NAME}`, where `{STATUS}` is the status + /// (`update` if the port is being updated or `new` if a new port is + /// being created), `{CATEGORY}` is the port category, and `{NAME}` is + /// the port name. + pub fn create_branch(&mut self, port: &TmpPort) -> Result<(), CportError> { + self.set_current_dir(&self.repo_root_pth, "Git::create_branch")?; + let branch_name = format!( + "mkport/{}/{}-{}", + if self.check_exists(port) { + "update" + } else { + "new" + }, + &port.package.category, + &port.package.name + ); + self.created_branch = Some(branch_name.clone()); + + Command::new(GIT) + .arg("branch") + .arg(&branch_name) + .status() + .map_err(|err| CportError::GitError(err.to_string()))?; + + Command::new(GIT) + .arg("checkout") + .arg(&branch_name) + .status() + .map_err(|err| CportError::GitError(err.to_string()))?; + + self.set_current_dir(&self.current_dir, "Git::create_branch")?; + Ok(()) + } + + /// Adds all modified, deleted and created files to the index and creates a commit. + pub fn create_commit(&self, msg: &'a str) -> Result<(), CportError> { + self.set_current_dir(&self.repo_root_pth, "Git::create_commit")?; + + Command::new(GIT) + .arg("add") + .arg(".") + .status() + .map_err(|err| CportError::GitError(err.to_string()))?; + + Command::new(GIT) + .arg("commit") + .arg("-m") + .arg(msg) + .status() + .map_err(|err| CportError::GitError(err.to_string()))?; + + self.set_current_dir(&self.current_dir, "Git::create_commit")?; + Ok(()) + } + + /// Sends all changes to GitLab + pub fn push_changes(&self) -> Result<(), CportError> { + self.set_current_dir(&self.repo_root_pth, "Git::push_changes")?; + let branch = &self.created_branch.clone().ok_or(CportError::DataMissing( + "The branch name is not specified".to_string(), + ))?; + + Command::new(GIT) + .arg("push") + .arg("--set-upstream") + .arg("origin") + .arg(branch) + .status() + .map_err(|err| CportError::GitError(err.to_string()))?; + + self.set_current_dir(&self.current_dir, "Git::push_changes")?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::process::Command; + + #[test] + fn git_new_test() { + let mut git = Git::new( + "https://gitlab.com/calmiralinux/cabs/ports", + "fix/xorg", + "/tmp/ports", + ) + .unwrap(); + git.create_branch(&TmpPort::default()).unwrap(); + Command::new("cp") + .arg("/etc/fstab") + .arg("/tmp/ports/fstab.txt") + .status() + .unwrap(); + git.create_commit("some changes...").unwrap(); + } +} diff --git a/mkport/src/lib.rs b/mkport/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..335e75384660ca6b82c440c047a99c0cbc21ae75 --- /dev/null +++ b/mkport/src/lib.rs @@ -0,0 +1,216 @@ +//! `mkport` is a tool for automating the creation of new ports. It asks the user for basic information about the port and then generates the contents of the port on its own, based on the received data. +//! +//! The program can test the build of a port, test if the port is built correctly, generate a `files.list` file and send changes to the remote port system repository. + +pub mod bs; +pub mod build; +pub mod conf; +pub mod git; +pub mod tmp_port; + +pub mod tui; + +use build::Build; +use conf::LocalPortConf; +use git::Git; +use libmsg::Message; +use std::path::Path; +use std::process::Command; + +use bs::get_install_content; +use bs::BuildSystem; +use bs::Maintainer; +use conf::GlobalConf; +use tmp_port::create_install; +use tmp_port::get_editor; +use tmp_port::TmpPort; + +pub fn repo_clone(git: &mut Git, conf: &GlobalConf, port: &TmpPort) -> Result { + let msg = Message::new(0, "git"); + + msg.msg("Clone repository..."); + let repo_dir = git.repo_clone().map_err(|err| { + format!( + "Failed to run child process «/usr/bin/git»: {}", + err.to_string() + ) + })?; + + msg.msg(format!("Create new branch «{}»", &conf.branch)); + git.create_branch(&port).map_err(|err| { + format!( + "Failed to run hild process «/usr/bin/git»: {}", + err.to_string() + ) + })?; + + Ok(repo_dir) +} + +pub fn commit_changes(git: &mut Git, conf: &GlobalConf, port: &TmpPort) -> Result<(), String> { + let mut msg = Message::new(0, "git"); + + msg.msg("Create git commit..."); + let git_msg = format!( + "Create port '{}/{}'\n\n- **Version:** {}\n- **Description:** {}", + &port.package.category, + &port.package.name, + &port.package.version, + &port.package.description, + ); + + git.create_commit(&git_msg).map_err(|err| err.to_string())?; + + if conf.autopush { + msg.msg("Push changes..."); + git.push_changes().map_err(|err| err.to_string())?; + } else { + msg.del_oper(); + msg.warn("WARNING: to push changes run this commands:\n"); + println!("\t$ cd {}", &git.repo_root_pth); + println!( + "\t$ git push --set-upstream origin {}\n", + // после исполнения сборочных + // инструкций этот параметр + // равен `Some(String)`. + &git.created_branch.clone().unwrap() + ); + } + + Ok(()) +} + +pub fn create_port_files( + port: &TmpPort, + conf: &GlobalConf, + maintainer: &Maintainer, + build_sys: BuildSystem, + repository_pth: &str, +) -> Result<(), String> { + let mut msg = Message::new(0, "create port files"); + + let port_dir_pth_str = format!( + "{}/ports/{}/{}", + repository_pth, &port.package.category, &port.package.name, + ); + let port_dir_pth = Path::new(&port_dir_pth_str); + + // Если мы создаём новый порт, то нужна директория, где будут располагаться + // его файлы. Если мы изменяем уже созданный порт, то создавать директорию + // для него уже не требуется. + if !port_dir_pth.exists() { + msg.msg(format!("Create port directory '{port_dir_pth_str}'...")); + std::fs::create_dir_all(port_dir_pth) + .map_err(|err| format!("Error: {}", err.to_string()))?; + } + + let install_pth_str = format!("{port_dir_pth_str}/install"); + let install_pth = Path::new(&install_pth_str); + + // В случае отсутствия файла `install` (т.е. когда создаём новый порт, + // а не изменяем уже существующий), нужно его сгенерировать и записать. + // Если мы изменяем существующий порт, то этого делать не нужно. + if !install_pth.exists() { + msg.msg("Generate initial 'install' file..."); + let install_str = get_install_content(build_sys, port, maintainer) + .map_err(|err| format!("Failed: {}", err.to_string()))?; + + msg.msg("Write generated content to the 'install' file..."); + create_install(install_pth, &install_str) + .map_err(|err| format!("Failed: {}", err.to_string()))?; + } + + msg.msg("Search editor program..."); + match get_editor(&conf.editor) { + None => { + msg.set_indent(2); + msg.error("Editor program not found", true); + } + Some(editor) => { + msg.set_indent(2); + msg.msg("Run editor..."); + msg.msg2(format!("editor: {editor}")); + msg.msg2(format!("inst_path: {install_pth_str}")); + + let status = Command::new(&editor) + .arg(&install_pth_str) + .status() + .map_err(|err| { + format!( + "Error while the child process is running: {}", + err.to_string() + ) + })?; + + if status.success() { + msg.ok("Write changes to disk"); + } else { + msg.error( + format!("error while a third-party process is running: {editor}"), + true, + ); + } + } + } + + Ok(()) +} + +pub fn build_port(port: &TmpPort, lconf: &LocalPortConf, ports_pth: &str) -> Result<(), String> { + let mut msg = Message::new(0, "build"); + + msg.msg("Setting up build machine"); + let port_pth = format!("{}/ports/", &ports_pth); + let build_machine = Build::new(&port, &port_pth) + .set_installation_pth(&lconf.installation_dir) + .set_src_pth(&lconf.src_dir); + + msg.msg("Build port..."); + + msg.set_indent(2); + msg.msg("Download port..."); + if &port.port.url != "" { + build_machine + .download() + .map_err(|err| format!("Failed to download file: {}", err.to_string()))?; + } else { + msg.msg2("Nothing to do"); + } + + msg.msg("Build port from sources..."); + let result = build_machine + .build(lconf.threads, lconf.tests) + .map_err(|err| { + format!( + "Error while the child process is running: {}", + err.to_string() + ) + })?; + + if result.code != 0 { + msg.error("Build port from sources failed!", false); + std::process::exit(result.code); + } else { + msg.ok("Build port OK"); + } + + let mut usage = 0.; + if lconf.files_list_generation { + msg.msg("Generate «files.list»..."); + usage = build_machine + .gen_flist() + .map_err(|err| format!("Generation («files.list») failed: {}", err.to_string()))?; + } + + msg.msg("Generate «port.toml»..."); + build_machine + .gen_ptoml(usage, result.time) + .map_err(|err| format!("Generation («port.toml») failed: {}", err.to_string()))?; + + msg.msg("Generate «README.md»..."); + build_machine + .gen_markdown() + .map_err(|err| format!("Generation («README.md») failed: {}", err.to_string()))?; + + Ok(()) +} diff --git a/mkport/src/main.rs b/mkport/src/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..97ae8fabaf072ca4f81e2c68737475f63afe194c --- /dev/null +++ b/mkport/src/main.rs @@ -0,0 +1,128 @@ +use mkport::bs::BuildSystem; +use mkport::bs::Maintainer; +use mkport::conf::GlobalConf; +use mkport::conf::LocalPortConf; +use mkport::git::Git; +use mkport::tui::master_layout; + +use std::fs::remove_file; +use std::path::Path; +use std::process; + +use libcport::traits::TomlConfig; +use libmsg::Message; + +fn main() { + let mut msg = Message::default(); + msg.msg("Requesting port data..."); + match master_layout() { + Some(port) => { + let conf = match GlobalConf::parse("./mkport.conf") { + Ok(c) => c, + Err(why) => { + msg.warn(format!( + "Error parsing the «./mkport.conf» file: {}. Use the default parameters.", + why.to_string() + )); + GlobalConf::default() + } + }; + let lconf = match LocalPortConf::parse("./.mkport.conf") { + Ok(c) => c, + Err(why) => { + msg.warn(format!( + "Error parsing the «./.mkport.conf» file: {}. Use the default parameters.", + why.to_string() + )); + LocalPortConf::default() + } + }; + + msg.set_oper("git"); + let mut git = match Git::new( + "https://gitlab.com/calmiralinux/cabs/Ports", + &conf.branch, + &conf.repo_root_pth, + ) { + Ok(g) => g, + Err(why) => { + msg.error( + format!("Error connecting to `git`: {}", why.to_string()), + false, + ); + std::process::exit(1) + } + }; + + let repo_dir = match mkport::repo_clone(&mut git, &conf, &port) { + Ok(repo) => repo, + Err(why) => { + msg.error( + format!("Failed to run child process «/usr/bin/git»: {}", why), + false, + ); + std::process::exit(1) + } + }; + + let maintainer = Maintainer { + years: "2021-2023".to_string(), + name: conf.maintainer.clone(), + }; + + let build_sys = match &port.package.build_sys as &str { + "meson" => BuildSystem::Meson, + "cargo" => BuildSystem::Cargo, + _ => BuildSystem::Make, + }; + + // Создаём файлы порта + if let Err(why) = + mkport::create_port_files(&port, &conf, &maintainer, build_sys, &repo_dir) + { + msg.set_indent(4); + msg.error(why, true); + } + + // Собираем порт и генерируем остальные нужные нам файлы + if let Err(why) = mkport::build_port(&port, &lconf, &git.repo_root_pth) { + msg.set_indent(4); + msg.error(why, true); + } + + if let Err(why) = mkport::commit_changes(&mut git, &conf, &port) { + msg.set_indent(4); + msg.error( + format!("Failed to run child process «/usr/bin/git»: {}", why), + true, + ); + } + } + None => { + msg.error("Some data was not received or the user aborted the port build on their own. Aborting...", false); + + if let Err(why) = clean() { + msg.set_indent(2); + msg.error(why.to_string(), true); + } + + process::exit(1); + } + } + + if let Err(why) = clean() { + msg.set_indent(2); + msg.error(why.to_string(), true); + } +} + +fn clean() -> Result<(), String> { + let msg = Message::default(); + msg.msg2("Remove temp files..."); + if Path::new("./.port.toml").exists() { + remove_file("./.port.toml") + .map_err(|err| format!("Failed to remove «./.port.toml»: {}", err.to_string()))?; + } + + Ok(()) +} diff --git a/mkport/src/tmp_port.rs b/mkport/src/tmp_port.rs new file mode 100644 index 0000000000000000000000000000000000000000..3827f6c8f82c051cbfe999c1035c7044a054cfa2 --- /dev/null +++ b/mkport/src/tmp_port.rs @@ -0,0 +1,461 @@ +use libcport::port::DepsConfigSection; +use libcport::port::Port; +use libcport::port::PortConfig; +use libcport::port::PortConfigSection; + +use libcport::errors::CportError; +use libcport::traits::TomlConfig; + +use serde::Deserialize; +use serde::Serialize; + +use std::env; +use std::fs; +use std::path::Path; +use toml; + +const UNSUPPORTED_CHARS: [char; 21] = [ + ',', '.', '?', '<', '>', '"', '\'', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '-', '+', + ':', ';', +]; + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct TmpPort { + pub package: PackageSection, + pub deps: DepsSection, + pub port: PortSection, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct PackageSection { + pub category: String, + pub name: String, + pub version: String, + pub description: String, + pub maintainers: String, + pub priority: String, + pub usage: f64, + pub upgrade_mode: String, + pub build_time: f64, + pub build_sys: String, +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct DepsSection { + pub required: String, + pub recommend: String, + pub optional: String, +} + +#[derive(Debug, Deserialize, Serialize, Default, Clone)] +pub struct PortSection { + pub url: String, + pub file: String, + pub md5: String, + pub sha256: String, +} + +#[derive(Debug)] +pub enum Warnings<'a> { + /// Use of a large number of unnecessary port dependencies. + /// + /// A warning is shown when any parameter in the `deps` section + /// contains dependencies from the `base/` category. + /// + /// ## Value: + /// + /// 1. String from parameter containing unnecessary ports. + /// 2. Dependency type + OverDependencies(&'a str, &'a str), + + /// Very long port description. + /// + /// A warning is shown when the port description is more than + /// 80 characters long. + /// + /// ## Value: + /// + /// Description text. + LongDescription(&'a str), + + /// Using unnecessary `port.file` parameter. + /// + /// The `port.file` parameter is unnecessary if it is not equal + /// to `Option::None` and the file name can be calculated from + /// the `port.url` parameter. + /// + /// It is considered impossible to compute the file name from + /// the `port.url` parameter in the following cases: + /// + /// 1. When there is no mention of the file in the parameter (URL). + /// 2. When the URL leads to GitHub, GitLab or Sourceforge. + /// + /// ## Value: + /// + /// 1. URL from the `port.url` parameter. + /// 2. The name of the file extracted from the URL. + UnnecessaryParameter(&'a str, &'a str), +} + +#[derive(Debug)] +pub enum Errors<'a> { + /// The required parameter is empty or contains only whitespace characters + /// + /// ## Value: + /// + /// Parameter name + EmptyParam(&'a str), + + /// Parameter data contains invalid or unsupported characters + /// + /// ## Value: + /// + /// 1. Parameter name. + /// 2. Parameter value. + HasUnsupportedChars(&'a str, &'a str), + + InvalidFormat(&'a str, &'a str), +} + +impl<'a> Errors<'a> { + pub fn check(port: &'a TmpPort) -> Vec { + let mut result = Vec::new(); + + if Self::is_empty(&port.package.category) { + result.push(Self::EmptyParam("Category")); + } + if Self::is_empty(&port.package.name) { + result.push(Self::EmptyParam("Name")); + } + if Self::is_empty(&port.package.version) { + result.push(Self::EmptyParam("Version")); + } + if Self::is_empty(&port.package.description) { + result.push(Self::EmptyParam("Description")); + } + if Self::is_empty(&port.package.maintainers) { + result.push(Self::EmptyParam("package.maintainers")); + } + if Self::is_empty(&port.package.priority) { + result.push(Self::EmptyParam("Priority")); + } + if Self::is_empty(&port.package.upgrade_mode) { + result.push(Self::EmptyParam("package.upgrade_mode")); + } + if Self::is_empty(&port.package.build_sys) { + result.push(Self::EmptyParam("Build system")); + } + + if Self::has_unsupported(&port.deps.required) { + result.push(Self::HasUnsupportedChars("required", &port.deps.required)); + } + if Self::has_unsupported(&port.deps.recommend) { + result.push(Self::HasUnsupportedChars("recommend", &port.deps.recommend)); + } + if Self::has_unsupported(&port.deps.optional) { + result.push(Self::HasUnsupportedChars("optional", &port.deps.optional)); + } + + if !["user".to_string(), "system".to_string()].contains(&port.package.priority) { + result.push(Self::InvalidFormat("Priority", &port.package.priority)); + } + if !["soft".to_string(), "hard".to_string()].contains(&port.package.upgrade_mode) { + result.push(Self::InvalidFormat( + "package.upgrade_mode", + &port.package.upgrade_mode, + )); + } + + result + } + + fn has_unsupported(param_data: &'a str) -> bool { + for sym in UNSUPPORTED_CHARS { + if param_data.contains(sym) { + return true; + } + } + false + } + + fn is_empty(param_data: &'a str) -> bool { + param_data.is_empty() + } +} + +impl<'a> Warnings<'a> { + pub fn check(port: &'a TmpPort) -> Vec { + let mut result = Vec::new(); + if let Some(descr) = Self::check_description(&port.package.description) { + result.push(descr); + } + if let Some(param) = Self::check_param(&port.port) { + result.push(param); + } + for deps in Self::check_deps(&port.deps) { + result.push(deps); + } + result + } + + fn check_deps(deps: &'a DepsSection) -> Vec { + let mut chck = Vec::new(); + if deps.required.contains("base/") { + chck.push(Self::OverDependencies(&deps.required, "required")); + } + if deps.recommend.contains("base/") { + chck.push(Self::OverDependencies(&deps.recommend, "recommend")); + } + if deps.optional.contains("base/") { + chck.push(Self::OverDependencies(&deps.optional, "optional")); + } + chck + } + + fn check_description(descr: &'a str) -> Option { + if descr.len() > 80 { + Some(Self::LongDescription(descr)) + } else { + None + } + } + + fn check_param(port: &'a PortSection) -> Option { + if let Some((_, fname)) = port.url.rsplit_once('/') { + if fname == port.file { + Some(Self::UnnecessaryParameter(&port.url, fname)) + } else { + None + } + } else { + None + } + } +} + +impl TomlConfig for TmpPort {} + +impl TmpPort { + pub fn write_new(&self, path: &str) -> Result<(), CportError> { + let str_data = toml::to_string(&Self::default()) + .map_err(|err| CportError::Serialize(err.to_string()))?; + fs::write(path, str_data).map_err(|err| CportError::Serialize(err.to_string()))?; + Ok(()) + } + + pub fn try_to_port(&self) -> Result { + Port::try_new(&format!( + "{}/{}", + &self.package.category, &self.package.name + )) + } + + fn get_deps(&self, deps: &String) -> Option> { + if deps.is_empty() { + None + } else { + let deps = deps.split(' '); + Some(deps.map(|s| s.trim().to_string()).collect()) + } + } + + fn md5>(&self, fname: P) -> Result { + let file = fs::read(&fname).map_err(|err| { + CportError::IO( + "read".to_string(), + fname + .as_ref() + .to_str() + .unwrap_or("unknown file") + .to_string(), + err.to_string(), + ) + })?; + let digest = md5::compute(file); + Ok(format!("{:x}", digest)) + } + + fn sha256>(&self, fname: P) -> Result { + let file = fs::read(&fname).map_err(|err| { + CportError::IO( + "read".to_string(), + fname + .as_ref() + .to_str() + .unwrap_or("unknown file") + .to_string(), + err.to_string(), + ) + })?; + let digest = sha256::digest(file); + Ok(digest) + } + + pub fn to_pconf(&self, calculate_hashs: bool) -> Result { + let mut pconf = PortConfig::default(); + pconf.package.name = self.package.name.clone(); + pconf.package.version = self.package.version.clone(); + pconf.package.description = self.package.description.clone(); + pconf.package.usage = self.package.usage; + pconf.package.build_time = self.package.build_time; + pconf.package.priority = self.package.priority.clone(); + + if pconf.package.maintainers.is_empty() { + pconf.package.maintainers = vec![self.package.maintainers.clone()]; + } else { + /* + * В том случае, если параметр `pconf.package.maintainers` уже содержит имя + * сопровождающего, которое мы пытаемся туда добавить, то это имя во второй + * раз добавлять не нужно. + */ + if !pconf + .package + .maintainers + .contains(&self.package.maintainers) + { + pconf + .package + .maintainers + .push(self.package.maintainers.clone()); + } + } + + let req_deps = self.get_deps(&self.deps.required); + let rec_deps = self.get_deps(&self.deps.recommend); + let opt_deps = self.get_deps(&self.deps.optional); + + if req_deps.is_none() && rec_deps.is_none() && opt_deps.is_none() { + pconf.deps = None; + } else { + pconf.deps = Some(DepsConfigSection { + required: req_deps, + recommend: rec_deps, + optional: opt_deps, + }); + } + + pconf.port = if self.port.url.is_empty() { + None + } else { + let fname = if self.port.file.is_empty() { + self.port.url.rsplit_once('/').unwrap_or(("", "tmp.bin")).1 + } else { + &self.port.file + }; + + let md5 = if calculate_hashs && Path::new(fname).is_file() { + Some(self.md5(fname)?) + } else { + None + }; + let sha256 = if calculate_hashs && Path::new(fname).is_file() { + Some(self.sha256(fname)?) + } else { + None + }; + + Some(PortConfigSection { + url: Some(self.port.url.clone()), + file: if self.port.file.is_empty() { + None + } else { + Some(self.port.file.clone()) + }, + md5, + sha256, + }) + }; + + Ok(pconf.clone()) + } + + pub fn exists>(&self, git: P) -> bool { + let pdir = format!("{}/{}", &self.package.category, &self.package.name); + git.as_ref().join(pdir).is_dir() + } + + pub fn create_port_dir>(&self, git: P) -> Result<(), CportError> { + let pdir = format!("{}/{}", &self.package.category, &self.package.name); + let pdir_pth = git.as_ref().join(pdir); + fs::create_dir_all(pdir_pth).map_err(|err| CportError::CreateDir(err.to_string()))?; + Ok(()) + } +} + +impl Default for PackageSection { + fn default() -> Self { + Self { + category: "general".to_string(), + name: "".to_string(), + version: "".to_string(), + description: "".to_string(), + maintainers: "".to_string(), + priority: "user".to_string(), + usage: 0., + upgrade_mode: "soft".to_string(), + build_time: 0., + build_sys: "make".to_string(), + } + } +} + +pub fn create_install>(pth: P, cont: &str) -> Result<(), CportError> { + fs::write(pth, cont).map_err(|err| CportError::CreateFile(err.to_string()))?; + Ok(()) +} + +fn search_editor() -> Option { + let pathes = [ + "/usr/bin/vim", + "/usr/bin/nvim", + "/usr/bin/helix", + "/usr/bin/hx", + "/usr/bin/mcedit", + "/usr/bin/nano", + "/usr/bin/micro", + "/usr/bin/gedit", + "/usr/bin/gnome-text-editor", + ]; + for pth in pathes { + if Path::new(pth).exists() { + return Some(pth.to_string()); + } + } + None +} + +fn get_editor_pth() -> Option { + match env::var("EDITOR") { + Ok(mut e) => { + // В пути до текстового редактора может находиться последовательность ~/ + // + // ~/ - синтаксический сахар Shell. `mkport` не знает, что такое ~/, + // поэтому требуется заменять его на путь до текущей директории пользователя. + if e.contains("~/") { + let home = env::var("HOME").unwrap_or("/tmp".to_string()); + e = e.replace("~/", &format!("{home}/")); + } + + if !Path::new(&e).exists() { + search_editor() + } else { + Some(e) + } + } + Err(_) => search_editor(), + } +} + +pub fn get_editor(editor: &Option) -> Option { + match editor { + Some(ed) => { + let default_editor_pth = Path::new(ed); + if default_editor_pth.exists() { + Some(ed.to_string()) + } else { + get_editor_pth() + } + } + None => get_editor_pth(), + } +} diff --git a/mkport/src/tui.rs b/mkport/src/tui.rs new file mode 100644 index 0000000000000000000000000000000000000000..5434656cf3b92b9ccf8db96b75fac1f75b832f43 --- /dev/null +++ b/mkport/src/tui.rs @@ -0,0 +1,109 @@ +//! Forms the `mkport` pseudo-graphic interface + +use libmsg::tui::Release; +use libmsg::tui::Tui; + +use cursive::event::Key; +use cursive::menu::Tree; +use cursive::traits::With; +use cursive::Cursive; + +pub mod help_windows; +pub mod master_windows; +pub mod preferences_windows; +pub mod service_windows; + +use crate::tmp_port::Errors; +use crate::tmp_port::TmpPort; +use libcport::traits::TomlConfig; + +use self::help_windows::about_window; +use self::help_windows::shortcuts_window; + +use self::preferences_windows::global_conf_window; +use self::preferences_windows::local_conf_window; + +use self::service_windows::exit_window; +use self::service_windows::initial_window; +use self::service_windows::unlock_master_window; + +use self::master_windows::enter_port_data_window; + +fn menubar(scr: &mut Cursive) { + scr.menubar() + .add_subtree( + "Port", + Tree::new() + .leaf("New [+]", enter_port_data_window) + .leaf("Quit [F10]", |s| { + exit_window(s, "Really exit? Any changes you made will not be saved.") + }), + ) + .add_subtree( + "Edit", + Tree::new() + .leaf("Global preferences [F2]", global_conf_window) + .leaf("Local preferences [F3]", local_conf_window) + .with(|menu| { + let ver = env!("CARGO_PKG_VERSION"); + if ver.contains('a') || ver.contains('b') { + menu.add_leaf("Force unlock mkport [DEV] [F12]", unlock_master_window); + } + }), + ) + .add_subtree( + "Help", + Tree::new() + .leaf("Shortcuts [F9]", shortcuts_window) + .leaf("About", about_window), + ); + + scr.set_autohide_menu(false); + scr.add_global_callback(Key::F1, |s| s.select_menubar()); + scr.add_global_callback(Key::F2, global_conf_window); + scr.add_global_callback(Key::F3, local_conf_window); + scr.add_global_callback(Key::F8, |s| { + let _ = s.menubar().with(|menu| { + let label = "Force unlock mkport [DEV]"; + if menu.find_position(label).is_none() { + menu.add_leaf(label, |s| { + unlock_master_window(s); + if let Some(pos) = s.menubar().find_position(label) { + s.menubar().remove(pos); + } + enter_port_data_window(s); + }); + } + }); + }); + scr.add_global_callback(Key::F9, shortcuts_window); + scr.add_global_callback(Key::F10, |s| { + exit_window(s, "Really exit? Any changes you made will not be saved.") + }); + scr.add_global_callback('q', |s| { + exit_window(s, "Really exit? Any changes you made will not be saved.") + }); + scr.add_global_callback(Key::F12, unlock_master_window); + scr.add_global_callback('+', enter_port_data_window); +} + +pub fn master_layout() -> Option { + let mut tui = Tui::new(Release::detect_release_type(env!("CARGO_PKG_VERSION"))).init(); + tui.cursive.set_window_title("Port Wizard"); + menubar(&mut tui.cursive); + initial_window(&mut tui.cursive); + enter_port_data_window(&mut tui.cursive); + + tui.cursive.run(); + + match TmpPort::parse("./.port.toml") { + Ok(port) => { + if Errors::check(&port).is_empty() { + Some(port) + } else { + None + } + } + Err(_) => None, + } +} diff --git a/mkport/src/tui/help_windows.rs b/mkport/src/tui/help_windows.rs new file mode 100644 index 0000000000000000000000000000000000000000..a1f83ee38adfc82e98cafb14dab71b9b971d1b27 --- /dev/null +++ b/mkport/src/tui/help_windows.rs @@ -0,0 +1,61 @@ +use cursive::views::ListView; +use libmsg::tui::about::AboutWindowBuilder; +use libmsg::tui::about::Copyright; +use libmsg::tui::about::Donation; + +use cursive::theme::Effect; +use cursive::utils::markup::StyledString; +use cursive::views::Dialog; +use cursive::views::TextView; +use cursive::Cursive; + +pub fn shortcuts_window(scr: &mut Cursive) { + let mut shortcuts = ListView::new() + .child( + "Create new port", + TextView::new(StyledString::styled("+", Effect::Bold)), + ) + .delimiter() + .child("Select menubar", TextView::new("F1")) + .delimiter() + .child("Global preferences", TextView::new("F2")) + .delimiter() + .child("Local preferences", TextView::new("F3")) + .delimiter() + .child("Exit mkport", TextView::new("F10")); + let ver = env!("CARGO_PKG_VERSION"); + if ver.contains('a') || ver.contains('b') { + shortcuts.add_child("Force unlock mkport [DEV]", TextView::new("F12/F8")); + } + let win = Dialog::around(shortcuts) + .title("Shortcuts") + .button("OK", |s| { + s.pop_layer(); + }); + scr.add_layer(win); +} + +pub fn about_window(scr: &mut Cursive) { + let mut about = AboutWindowBuilder::new(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")) + .set_description(env!("CARGO_PKG_DESCRIPTION")) + .set_site("https://gitlab.com/calmiralinux/cabs/cport") + .add_copyright(Copyright::new( + "2021-2023", + "Michail Krasnov", + "linuxoid85@gmail.com", + )) + .add_donation(Donation::new( + "Michail Krasnov", + "Сбербанк", + "2202 2062 5233 5406", + )) + .build(); + + let win = Dialog::around(about.window()) + .title("About «Port Wizard»") + .button("OK", |s| { + s.pop_layer(); + }); + + scr.add_layer(win); +} diff --git a/mkport/src/tui/master_windows.rs b/mkport/src/tui/master_windows.rs new file mode 100644 index 0000000000000000000000000000000000000000..c13a07cb507a2c38079c5460ac21703be01d2ee4 --- /dev/null +++ b/mkport/src/tui/master_windows.rs @@ -0,0 +1,319 @@ +use cursive::view::Resizable; +use cursive::views::Dialog; +use cursive::views::EditView; +use cursive::views::LinearLayout; +use cursive::views::ListView; +use cursive::views::Panel; +use cursive::views::SelectView; +use cursive::views::TextView; +use cursive::Cursive; + +use cursive::traits::Nameable; +use cursive::traits::Scrollable; +use cursive::traits::With; + +use libcport::consts::METADATA_FILE; +use libcport::errors::CportError; +use libcport::metadata::Metadata; +use libcport::traits::TomlConfig; +use libmsg::tui::error::ErrorBuilder; + +use crate::conf::GlobalConf; +use crate::tmp_port::DepsSection; +use crate::tmp_port::Errors; +use crate::tmp_port::PackageSection; +use crate::tmp_port::PortSection; +use crate::tmp_port::TmpPort; +use crate::tmp_port::Warnings; + +use super::service_windows::cancel_operation; +use super::service_windows::config_parse_error_window; +use super::service_windows::data_error_window; +use super::service_windows::errors_window; +use super::service_windows::is_lock; +use super::service_windows::is_port_temp_data; +use super::service_windows::lock_master_window; +use super::service_windows::unlock_master_window; +use super::service_windows::warnings_window; + +const PRIORITY: [&str; 2] = ["user", "system"]; +const BUILD_SYS: [&str; 4] = ["make", "meson", "cargo", "other"]; + +fn get_category_idx(list: &[String], category: &str) -> Option { + for (i, cat) in list.iter().enumerate() { + if *cat == category { + return Some(i); + } + } + None +} + +fn write_port_temp_data(scr: &mut Cursive, port: &TmpPort) { + if let Err(why) = port.write("./.port.toml") { + let mut err = ErrorBuilder::new("Failed to write temp port data!") + .set_traceback_text(&why.to_string()) + .build(); + let win = Dialog::around(err.window()) + .title("Error") + .button("Quit", |s| s.quit()); + scr.add_layer(win); + } + scr.quit(); +} + +pub fn get_tmp_port_from_master_window( + scr: &mut Cursive, + conf: &GlobalConf, +) -> Result { + let port = TmpPort { + package: PackageSection { + category: scr + .call_on_name("package.category", |s: &mut SelectView| { + s.selection().unwrap_or_default().to_string() + }) + .ok_or_else(|| CportError::DataMissing("Category".to_string()))?, + name: scr + .call_on_name("package.name", |s: &mut EditView| { + s.get_content().as_ref().to_string() + }) + .ok_or_else(|| CportError::DataMissing("Name".to_string()))?, + version: scr + .call_on_name("package.version", |s: &mut EditView| { + s.get_content().as_ref().to_string() + }) + .ok_or_else(|| CportError::DataMissing("Version".to_string()))?, + description: scr + .call_on_name("package.description", |s: &mut EditView| { + s.get_content().as_ref().to_string() + }) + .ok_or_else(|| CportError::DataMissing("Description".to_string()))?, + maintainers: format!("{} <{}>", &conf.maintainer, &conf.email), + priority: scr + .call_on_name("package.priority", |s: &mut SelectView| { + s.selection().unwrap_or_default().to_string() + }) + .ok_or_else(|| CportError::DataMissing("Priority".to_string()))?, + usage: 0.0, + upgrade_mode: "soft".to_string(), + build_time: 0.0, + build_sys: scr + .call_on_name("package.build_sys", |s: &mut SelectView| { + s.selection().unwrap_or_default().to_string() + }) + .ok_or_else(|| CportError::DataMissing("Build system".to_string()))?, + }, + deps: DepsSection { + required: scr + .call_on_name("deps.required", |s: &mut EditView| { + s.get_content().as_ref().to_string() + }) + .unwrap_or_default(), + recommend: scr + .call_on_name("deps.recommend", |s: &mut EditView| { + s.get_content().as_ref().to_string() + }) + .unwrap_or_default(), + optional: scr + .call_on_name("deps.optional", |s: &mut EditView| { + s.get_content().as_ref().to_string() + }) + .unwrap_or_default(), + }, + port: PortSection { + url: scr + .call_on_name("port.url", |s: &mut EditView| { + s.get_content().as_ref().to_string() + }) + .unwrap_or_default(), + file: scr + .call_on_name("port.file", |s: &mut EditView| { + s.get_content().as_ref().to_string() + }) + .unwrap_or_default(), + md5: "".to_string(), + sha256: "".to_string(), + }, + }; + Ok(port) +} + +fn cancel_master_window(scr: &mut Cursive) { + let text = TextView::new("Really cancel? Any changes will not be saved"); + let win = Dialog::around(text) + .button("Yes", |s| { + if is_lock() { + unlock_master_window(s); + } + if is_port_temp_data() { + cancel_operation(s); + } + s.pop_layer(); // Close dialog window + s.pop_layer(); // Close master window + }) + .button("No", |s| { + s.pop_layer(); + }); + scr.add_layer(win); +} + +fn errors_warnings_window(err: &Vec, wrn: &Vec) -> LinearLayout { + let err_win = errors_window(err); + let wrn_win = warnings_window(wrn); + + let mut err_wrn_win = LinearLayout::vertical(); + + if let Some(err) = err_win { + err_wrn_win.add_child(err); + } + + if let Some(wrn) = wrn_win { + err_wrn_win.add_child(wrn); + } + err_wrn_win +} + +fn create_port(scr: &mut Cursive, port: &TmpPort) { + let errors = Errors::check(port); + let warnings = Warnings::check(port); + + if errors.is_empty() && warnings.is_empty() { + unlock_master_window(scr); + write_port_temp_data(scr, port); + } else { + let err_wrn_win = errors_warnings_window(&errors, &warnings); + + if !err_wrn_win.is_empty() { + scr.add_layer( + Dialog::around(err_wrn_win.scrollable()) + .title("Errors and warnings") + .with(|d| { + if errors.is_empty() && !warnings.is_empty() { + // В случае если пользователь нажал на "Fix warnings", просто закрываем + // окно с предупреждениями и отображаем главное окно с полями ввода. + d.add_button("Fix warnings", |s| { + s.pop_layer(); + }); + + let port = port.clone(); + d.add_button("Ignore warnings", move |s| { + // В случае нажатия на кнопку "Ignore warnings" совершаем выход из + // псевдографики, пишем введённые пользователем данные во временный + // файл и приступаем к дальнейшей сборке порта. + write_port_temp_data(s, &port); + if is_lock() { + unlock_master_window(s); + } + }); + } + + if !errors.is_empty() { + // В случае возникновения ошибок не можем приступить к сборке порта, + // поэтому после того, как пользователь ознакомится с ошибками, + // требуется закрыть это окно и предоставить пользователю возможность + // исправления всех ошибок. + d.add_button("Fix all", |s| { + s.pop_layer(); + }); + } + }) + .button("Quit", |s| { + if is_lock() { + unlock_master_window(s); + } + if is_port_temp_data() { + cancel_operation(s); + } + s.quit() + }), + ); + } + } +} + +fn enter_window(scr: &mut Cursive) { + lock_master_window(scr); + + let mut priority = SelectView::new(); + priority.add_all_str(PRIORITY); + priority.set_selection(0); + + let mut build_sys = SelectView::new(); + build_sys.add_all_str(BUILD_SYS); + build_sys.set_selection(0); + + let metadata = Metadata::parse(METADATA_FILE).unwrap_or_default(); + let categories: Vec<&str> = metadata + .port_sys + .categories + .iter() + .map(|s| s.as_str()) + .collect(); + let mut category = SelectView::new(); + category.add_all_str(categories); + category.set_selection(get_category_idx(&metadata.port_sys.categories, "base/").unwrap_or(0)); + + let pkg_section = ListView::new() + .child("Name:", EditView::new().with_name("package.name")) + .child("Version:", EditView::new().with_name("package.version")) + .child( + "Description:", + EditView::new().with_name("package.description"), + ) + .child("Priority:", priority.popup().with_name("package.priority")) + .child("Category:", category.popup().with_name("package.category")) + .child( + "Build system:", + build_sys.popup().with_name("package.build_sys"), + ); + let deps_section = ListView::new() + .child("Required:", EditView::new().with_name("deps.required")) + .child("Recommend: ", EditView::new().with_name("deps.recommend")) + .child("Optional:", EditView::new().with_name("deps.optional")); + let port_section = ListView::new() + .child("URL:", EditView::new().with_name("port.url").min_width(40)) + .child("File name: ", EditView::new().with_name("port.file")); + + let layout = LinearLayout::vertical() + .child(Panel::new(pkg_section).title("Package")) + .child(Panel::new(deps_section).title("Dependencies")) + .child(Panel::new(port_section).title("Repository")) + .scrollable(); + + let win = Dialog::around(layout) + .title("Port Wizard") + .button("Create port", |s| { + let conf = match GlobalConf::parse("./mkport.conf") { + Ok(c) => c, + Err(why) => { + config_parse_error_window(s, why); + GlobalConf::default() + } + }; + + match get_tmp_port_from_master_window(s, &conf) { + Ok(port) => create_port(s, &port), + Err(why) => data_error_window(s, Some(why)), + } + }) + .button("Cancel", cancel_master_window); + scr.add_layer(win); +} + +pub fn enter_port_data_window(scr: &mut Cursive) { + if !is_lock() { + enter_window(scr); + } else { + let mut err = ErrorBuilder::new( + "You may already be creating a port! If\n\ + not, delete the following file:\n\n\ + /tmp/mkport.lock", + ) + .build(); + let win = Dialog::around(err.window()) + .title("Error") + .button("OK", |s| { + s.pop_layer(); + }); + scr.add_layer(win); + } +} diff --git a/mkport/src/tui/preferences_windows.rs b/mkport/src/tui/preferences_windows.rs new file mode 100644 index 0000000000000000000000000000000000000000..a040a6a1031d565796465ec94bfc68d5c3c93898 --- /dev/null +++ b/mkport/src/tui/preferences_windows.rs @@ -0,0 +1,287 @@ +use std::fmt::Display; + +use cursive::traits::Nameable; +use cursive::view::Resizable; +use cursive::views::Checkbox; +use cursive::views::Dialog; +use cursive::views::EditView; +use cursive::views::LinearLayout; +use cursive::views::ListView; +use cursive::views::Panel; +use cursive::views::SelectView; +use cursive::views::TextView; +use cursive::Cursive; + +use crate::conf::GlobalConf; +use crate::conf::LocalPortConf; +use libcport::errors::CportError; +use libcport::traits::TomlConfig; +use libmsg::tui::error::ErrorBuilder; + +static GLOBAL_CONF: &str = "./mkport.conf"; +static LOCAL_CONF: &str = "./.mkport.conf"; +static THREADS: [&str; 7] = ["1", "2", "4", "6", "8", "10", "12"]; + +fn write_to_global_conf_file(scr: &mut Cursive) -> Result<(), CportError> { + let global_conf = GlobalConf { + branch: scr + .call_on_name("global_conf.branch", |s: &mut EditView| s.get_content()) + .unwrap_or_default() + .to_string(), + repo_root_pth: scr + .call_on_name("global_conf.repo_root_pth", |s: &mut EditView| { + s.get_content() + }) + .unwrap_or_default() + .to_string(), + autopush: scr + .call_on_name("global_conf.autopush", |s: &mut Checkbox| s.is_checked()) + .unwrap_or_default(), + maintainer: scr + .call_on_name("global_conf.maintainer", |s: &mut EditView| s.get_content()) + .unwrap_or_default() + .to_string(), + email: scr + .call_on_name("global_conf.email", |s: &mut EditView| s.get_content()) + .unwrap_or_default() + .to_string(), + editor: { + let e = scr + .call_on_name("global_conf.editor", |s: &mut EditView| s.get_content()) + .unwrap_or_default() + .to_string(); + if e.is_empty() { + None + } else { + Some(e) + } + }, + }; + global_conf.write(GLOBAL_CONF)?; + Ok(()) +} + +fn write_to_local_conf_file(scr: &mut Cursive) -> Result<(), CportError> { + let local_conf = LocalPortConf { + files_list_generation: scr + .call_on_name("local_conf.files_list_generation", |s: &mut Checkbox| { + s.is_checked() + }) + .unwrap_or_default(), + tests: scr + .call_on_name("local_conf.tests", |s: &mut Checkbox| s.is_checked()) + .unwrap_or_default(), + threads: scr + .call_on_name("local_conf.threads", |s: &mut SelectView| { + // В строке нет ничего кроме чисел, поэтому метод + // parse() отработает без ошибок. На всякий случай, + // если в будущем будут добавлены новые варианты + // кол-ва потоков, в случае возникновения ошибки + // будет возвращено значение 1. + s.selection().unwrap_or_default().parse().unwrap_or(1) + }) + .ok_or_else(|| CportError::DataMissing("Threads".to_string()))?, + src_dir: scr + .call_on_name("local_conf.src_dir", |s: &mut EditView| { + s.get_content().as_ref().to_string() + }) + .unwrap_or_default(), + installation_dir: scr + .call_on_name("local_conf.installation_dir", |s: &mut EditView| { + s.get_content().as_ref().to_string() + }) + .unwrap_or_default(), + }; + local_conf.write(LOCAL_CONF)?; + Ok(()) +} + +fn error_write_window(scr: &mut Cursive, file: M, trace: M) { + let mut error = ErrorBuilder::new(&format!("Error while writing to «{file}»")) + .set_traceback_text(&format!("{trace}")) + .build(); + let win = Dialog::around(error.window()) + .title("Error") + .button("OK", |s| { + s.pop_layer(); + }); + scr.add_layer(win); +} + +fn show_global_conf_window(conf: &GlobalConf) -> Dialog { + let editor = match &conf.editor { + Some(e) => e, + None => "", + }; + let settings = ListView::new() + .child( + "Default branch:", + EditView::new() + .content(&conf.branch) + .with_name("global_conf.branch"), + ) + .child( + "Repository dir:", + EditView::new() + .content(&conf.repo_root_pth) + .with_name("global_conf.repo_root_pth"), + ) + .child( + "Maintainer name:", + EditView::new() + .content(&conf.maintainer) + .with_name("global_conf.maintainer"), + ) + .child( + "Maintainer mail:", + EditView::new() + .content(&conf.email) + .with_name("global_conf.email"), + ) + .child( + "Editor program:", + EditView::new() + .content(editor) + .with_name("global_conf.editor"), + ) + .child( + "Autopush [git]?", + Checkbox::new() + .with_checked(conf.autopush) + .with_name("global_conf.autopush"), + ); + let text = TextView::new(format!( + "Set the `mkport` parameters. They will be written to the file «{GLOBAL_CONF}»." + )); + + let layout = LinearLayout::vertical() + .child(text) + .child(Panel::new(settings)); + + Dialog::around(layout) + .title("Global preferences") + .button("Submit", |s| match write_to_global_conf_file(s) { + Ok(_) => { + s.pop_layer(); + } + Err(why) => error_write_window(s, GLOBAL_CONF, &why.to_string()), + }) + .button("Reset", |s| { + let conf = GlobalConf::default(); + s.pop_layer(); + s.add_layer(show_global_conf_window(&conf)); + }) + .button("Cancel", |s| { + s.pop_layer(); + }) +} + +fn get_thread_idx(list: &[&str; 7], threads: &str) -> Option { + for (i, thr) in list.iter().enumerate() { + if thr == &threads { + return Some(i); + } + } + None +} + +fn show_local_conf_window(conf: &LocalPortConf) -> Dialog { + let mut threads = SelectView::new(); + threads.add_all_str(THREADS); + threads.set_selection(get_thread_idx(&THREADS, &conf.threads.to_string()).unwrap_or(0)); + + let settings = ListView::new() + .child( + "Installation path:", + EditView::new() + .content(&conf.installation_dir) + .with_name("local_conf.installation_dir") + .min_width(20), + ) + .child( + "Source code path:", + EditView::new() + .content(&conf.src_dir) + .with_name("local_conf.src_dir"), + ) + .child( + "Generate «files.list»?", + Checkbox::new() + .with_checked(conf.files_list_generation) + .with_name("local_conf.files_list_generation"), + ) + .child( + "Tests?", + Checkbox::new() + .with_checked(conf.tests) + .with_name("local_conf.tests"), + ) + .child("Threads:", threads.popup().with_name("local_conf.threads")); + + Dialog::around(Panel::new(settings)) + .title("Local preferences") + .button("Submit", |s| match write_to_local_conf_file(s) { + Ok(_) => { + s.pop_layer(); + } + Err(why) => error_write_window(s, LOCAL_CONF, &why.to_string()), + }) + .button("Reset", |s| { + let conf = LocalPortConf::default(); + s.pop_layer(); + s.add_layer(show_local_conf_window(&conf)); + }) + .button("Cancel", |s| { + s.pop_layer(); + }) +} + +pub fn global_conf_window(scr: &mut Cursive) { + let win = match GlobalConf::parse(GLOBAL_CONF) { + Ok(conf) => show_global_conf_window(&conf), + Err(why) => { + let mut err = ErrorBuilder::new(&format!( + "Failed to parse global configuration file («{GLOBAL_CONF}»)!" + )) + .set_traceback_text(&why.to_string()) + .build(); + Dialog::around(err.window()) + .title("Error") + .button("Quit", |s| { + s.pop_layer(); + }) + .button("Reset", |s| { + let conf = GlobalConf::default(); + s.pop_layer(); + s.add_layer(show_global_conf_window(&conf)); + }) + } + }; + + scr.add_layer(win); +} + +pub fn local_conf_window(scr: &mut Cursive) { + let win = match LocalPortConf::parse(LOCAL_CONF) { + Ok(conf) => show_local_conf_window(&conf), + Err(why) => { + let mut err = ErrorBuilder::new(&format!( + "Failed to parse local [port] configuration file («{LOCAL_CONF}»)!" + )) + .set_traceback_text(&why.to_string()) + .build(); + Dialog::around(err.window()) + .title("Error") + .button("Quit", |s| { + s.pop_layer(); + }) + .button("Reset", |s| { + let conf = LocalPortConf::default(); + s.pop_layer(); + s.add_layer(show_local_conf_window(&conf)); + }) + } + }; + + scr.add_layer(win); +} diff --git a/mkport/src/tui/service_windows.rs b/mkport/src/tui/service_windows.rs new file mode 100644 index 0000000000000000000000000000000000000000..b0a6f430158877c84547a5acf3cc0dffb2d3090d --- /dev/null +++ b/mkport/src/tui/service_windows.rs @@ -0,0 +1,217 @@ +use cursive::views::LinearLayout; +use cursive::views::ScrollView; +use libcport::errors::CportError; +use libmsg::tui::error::ErrorBuilder; +use std::fs; +use std::path::Path; + +use cursive::align::HAlign; +use cursive::theme::Effect; +use cursive::traits::Scrollable; +use cursive::traits::With; +use cursive::utils::markup::StyledString; +use cursive::views::Dialog; +use cursive::views::Panel; +use cursive::views::TextView; +use cursive::Cursive; + +use crate::tmp_port::Errors; +use crate::tmp_port::Warnings; + +pub fn warnings_window(warn: &Vec) -> Option>> { + if warn.is_empty() { + None + } else { + let mut count = StyledString::styled("Warnings: ", Effect::Bold); + count.append_plain(warn.len().to_string()); + + let warnings_layout = LinearLayout::vertical() + .with(|s| { + for w in warn.iter().enumerate() { + let num = w.0 + 1; + match *w.1 { + Warnings::OverDependencies(deps, dep_type) => { + s.add_child( + Panel::new(TextView::new(deps)) + .title(format!("№{num}: Over deps: {dep_type}")) + .title_position(HAlign::Left), + ); + } + Warnings::LongDescription(descr) => { + s.add_child( + Panel::new(TextView::new(descr)) + .title(format!("№{num}: Very long description")) + .title_position(HAlign::Left), + ); + } + Warnings::UnnecessaryParameter(url, fname) => { + s.add_child( + Panel::new(TextView::new(format!("URL: {url}\nFile: {fname}",))) + .title(format!("№{num}: Unnecessary 'port.file' param")) + .title_position(HAlign::Left), + ); + } + } + } + }) + .scrollable(); + + Some( + Panel::new(warnings_layout) + .title(count) + .title_position(HAlign::Left), + ) + } +} + +pub fn errors_window(err: &Vec) -> Option>> { + if err.is_empty() { + None + } else { + let mut count = StyledString::styled("Errors: ", Effect::Bold); + count.append_plain(err.len().to_string()); + + let errors_layout = LinearLayout::vertical() + .with(|s| { + for e in err.iter().enumerate() { + let num = e.0 + 1; + match *e.1 { + Errors::EmptyParam(param) => { + s.add_child( + Panel::new(TextView::new(param)) + .title(format!("№{num}: Empty parameter")) + .title_position(HAlign::Left), + ); + } + Errors::HasUnsupportedChars(name, value) => { + s.add_child( + Panel::new(TextView::new(value)) + .title(format!( + "№{num}: Parameter '{name}' has unsupported chars" + )) + .title_position(HAlign::Left), + ); + } + Errors::InvalidFormat(name, value) => { + s.add_child( + Panel::new(TextView::new(value)) + .title(format!( + "№{num}: Invalid parameter of parameter '{name}'" + )) + .title_position(HAlign::Left), + ); + } + } + } + }) + .scrollable(); + + Some( + Panel::new(errors_layout) + .title(count) + .title_position(HAlign::Left), + ) + } +} + +pub fn initial_window(scr: &mut Cursive) { + let win = Panel::new(TextView::new( + "To open a menu, press the <↓>.\n\ + ——————————————————————————————————————————————\n\ + To create a port, open a window from the menu\n\ + «Port» → «New». To exit «Port» → «Quit» or\n\ + + or keyboard combinations.\n\ + ——————————————————————————————————————————————\n\ + Global mkport config file: './mkport.conf'\n", + )); + + scr.add_layer(win); +} + +pub fn exit_window(scr: &mut Cursive, msg: &str) { + let text = TextView::new(msg); + let win = Dialog::around(text) + .button("Yes", |s| { + if is_lock() { + unlock_master_window(s); + } + s.quit(); + }) + .button("No", |s| { + s.pop_layer(); + }); + scr.add_layer(win); +} + +pub fn data_error_window(scr: &mut Cursive, err: Option) { + let traceback_text = match err { + Some(e) => e.to_string(), + None => "Some data has not been entered! Please\ncheck the corectness of the entered\ndata before continuing".to_string(), + }; + let mut error = ErrorBuilder::new("Some data is missing or incorrect!") + .set_traceback_text(&traceback_text) + .build(); + let win = Dialog::around(error.window()) + .title("Data error") + .button("OK", |s| { + s.pop_layer(); + }); + scr.add_layer(win); +} + +pub fn config_parse_error_window(scr: &mut Cursive, err: CportError) { + let traceback = format!("{err}\n\nStandard parameters will be used"); + let mut error = ErrorBuilder::new("Config parse error") + .set_traceback_text(&traceback) + .build(); + let win = Dialog::around(error.window()) + .title("Parse error") + .button("OK", |s| s.quit()); + scr.add_layer(win); +} + +pub fn lock_master_window(scr: &mut Cursive) { + if let Err(why) = fs::write("/tmp/mkport.lock", "locked") { + let mut err = ErrorBuilder::new("Error lock master window!") + .set_traceback_text(&why.to_string()) + .build(); + let win = Dialog::around(err.window()) + .title("Error") + .button("Quit", |s| s.quit()); + scr.add_layer(win); + } +} + +pub fn unlock_master_window(scr: &mut Cursive) { + if let Err(why) = fs::remove_file("/tmp/mkport.lock") { + let mut err = ErrorBuilder::new("Error unlock master window!") + .set_traceback_text(&why.to_string()) + .build(); + let win = Dialog::around(err.window()) + .title("Error") + .button("Quit", |s| s.quit()); + scr.add_layer(win); + } +} + +pub fn cancel_operation(scr: &mut Cursive) { + if let Err(why) = fs::remove_file("./.port.toml") { + let mut error = ErrorBuilder::new("Failed to cancel operation") + .set_traceback_text(&why.to_string()) + .build(); + let win = Dialog::around(error.window()) + .title("Error") + .button("OK", |s| { + s.pop_layer(); + }); + scr.add_layer(win); + } +} + +pub fn is_port_temp_data() -> bool { + Path::new("./.port.toml").exists() +} + +pub fn is_lock() -> bool { + Path::new("/tmp/mkport.lock").is_file() +} diff --git a/pic/cport.png b/pic/cport.png deleted file mode 100644 index 5167463add3ff628463b7092021c8d9e7fbeae55..0000000000000000000000000000000000000000 Binary files a/pic/cport.png and /dev/null differ diff --git a/pic/cport1.png b/pic/cport1.png deleted file mode 100644 index 6db19fb38413459eea27644c52fa725fe9351498..0000000000000000000000000000000000000000 Binary files a/pic/cport1.png and /dev/null differ diff --git a/pic/cport_check.gif b/pic/cport_check.gif deleted file mode 100644 index f818dabad537546704b5ecdbfc9b1deccb184da5..0000000000000000000000000000000000000000 Binary files a/pic/cport_check.gif and /dev/null differ diff --git a/pic/cport_info.gif b/pic/cport_info.gif deleted file mode 100644 index c2ecc4c4d3755af308f6f73f045d70474e541835..0000000000000000000000000000000000000000 Binary files a/pic/cport_info.gif and /dev/null differ diff --git a/pic/cport_list.gif b/pic/cport_list.gif deleted file mode 100644 index bb694b0f36c5ccc75aadb3a294b37dcf0bb30dbf..0000000000000000000000000000000000000000 Binary files a/pic/cport_list.gif and /dev/null differ diff --git a/poc/Cargo.toml b/poc/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..27837c8848b1d04f81a4b155918e3882bce72add --- /dev/null +++ b/poc/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "poc" +version = "0.3.1" +edition = "2021" +description = "The Port Outdate Checker tool is to help ports to have an actual version." + +[[bin]] +name = "poc" + +[dependencies] +libcport = { version = "2", path = "../libcport" } + +clap.workspace = true +tokio.workspace = true +reqwest.workspace = true +serde.workspace = true +serde_json.workspace = true +toml.workspace = true +anyhow.workspace = true +home.workspace = true +log.workspace = true +env_logger.workspace = true +num_cpus.workspace = true +indicatif.workspace = true +console.workspace = true +tabled.workspace = true diff --git a/poc/Makefile b/poc/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..7d05d35e86e8627bda7361a346cbc70be98bbd38 --- /dev/null +++ b/poc/Makefile @@ -0,0 +1,13 @@ +build: + cargo build + +build.release: + cargo build --release + +install: + cp ../target/release/poc ~/.local/bin/poc + install -d -m755 ~/.config/poc/ + install -m644 files/*.toml ~/.config/poc/ + +clean: + cargo clean \ No newline at end of file diff --git a/poc/README.md b/poc/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ff50fb2618794bcdea37a46b8246ae85a2efd938 --- /dev/null +++ b/poc/README.md @@ -0,0 +1,170 @@ +# Port Outdate Checker (POC) + +The `poc` tool is for checking the `port` latest version from the Arch Linux repository. + +>Note: Arch Linux has a lot of software and updates very quickly. + +## Requirements +The program requires that the Calmira `Ports` repository is to be installed. +The `poc` tries to read path to the `Ports` from the `config.toml` file. +The configuration and list of mismatched names places in the `~/.config/poc/` direcory. + +## Usage +The `poc` CLI look like this: + +```shell +$ poc --help +The Port Outdate Checker tool is to help ports to have an actual version. + +Usage: poc [OPTIONS] <--table|--summary|--json> [version] + +Arguments: + + Port name in format "category/name" or "name" if option "category" is specified + + [version] + Port current version + +Options: + -t, --table + Format output as table(s) + + -s, --summary + A short summary + + -j, --json + Output in json format + + -f, --filter + [default: all] + + Possible values: + - all: Show all ports + - success: Show only success checks + - failed: Show only failed checks + - outdated: Show outdated ports only + + -c, --category + Port category to check for updates + + -S, --scan + Scan category to check for updates. (Default read from the metadata file) + +-a, --aur + Check for updates on the AUR server if Arch return an error + + -l, --log-level + Enable debug output + + [default: off] + [possible values: off, error, warn, info, debug] + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version +``` + +`port` +>If only `port` argument provided, the utility will read the current version from the `port.toml` file in the `Ports` repository and request the latest version from the Arch server. +Note: `port` must be in the format `category/name` + +`version` +>If `version` is provided, the utility will not read `port.toml` file and directly request the latest version from the Arch server and `port` must not be in format `category/name`. + +`-S`, `--scan` +> If `--scan` is provided, the utility will scan the specified category to create a list of ports otherwise it will read the `metadata.toml` file to create a list of ports. +This option will be ignored if the `--category` option is missing. + +`-a`, `--aur` +> If `--aur` is provided, the utility will make a request to the AUR server if the Arch server failed to find the specified port. + +## Examples +### Check one port and show result in the table with the default filer "All" +```bash +$ poc -t base/acl +┌──────┬─────────────────┬────────────────┐ +│ Name │ Current Version │ Latest Version │ +├──────┼─────────────────┼────────────────┤ +│ acl │ 2.3.1 │ 2.3.1 │ +└──────┴─────────────────┴────────────────┘ +``` +### Output in json format with the default filer "All" +```bash +$ poc --json base/acl +{"failures":[],"successes":[{"name":"acl","current_version":"2.3.1","latest_version":"2.3.1"}]} +``` + or formatted with `jq` + ```bash +$ poc --json base/acl | jq +{ + "failures": [], + "successes": [ + { + "name": "acl", + "current_version": "2.3.1", + "latest_version": "2.3.1" + } + ] +} +``` +### Use filter option to filter results +```bash +$ poc -jf success base/acl | jq +[ + { + "name": "acl", + "current_version": "2.3.1", + "latest_version": "2.3.1" + } +] +``` + +### Output in json format and show only success results +```bash +$ poc -jf success base/acl +[ + { + "name": "acl", + "current_version": "2.3.1", + "latest_version": "2.3.1" + } +] +``` + +### Check port with mismatched name +```bash +$ poc -t base/nvim +┌──────┬───────────────────────────────────┐ +│ Name │ Error │ +├──────┼───────────────────────────────────┤ +│ nvim │ No package found with name 'nvim' │ +└──────┴───────────────────────────────────┘ +``` +### Try to filter out failed checks +```bash +$ poc -tf success base/nvim +No successfully checked ports +``` + +### Check port without accessing `Ports` repository +```bash +$ poc -t gcc 0.0 +┌──────┬─────────────────┬────────────────┐ +│ Name │ Current Version │ Latest Version │ +├──────┼─────────────────┼────────────────┤ +│ gcc │ 0.0 │ 13.2.1 │ +└──────┴─────────────────┴────────────────┘ +``` +### Check category and print out summary +```bash +$ poc -cs postcpl + done [00:00:00] [#############################################################] 8/8 (0.0s) +Checked 8 ports +Successes: 0 +Failures: 8 +------------------------------------------------ +0 ports are up to date +0 ports need to be upgraded +``` \ No newline at end of file diff --git a/poc/files/config.toml b/poc/files/config.toml new file mode 100644 index 0000000000000000000000000000000000000000..fb819cedb26d6bb9d5164bb525a80b904f5dee4a --- /dev/null +++ b/poc/files/config.toml @@ -0,0 +1 @@ +ports_path = "/path/to/Ports" diff --git a/poc/files/mismatches.toml b/poc/files/mismatches.toml new file mode 100644 index 0000000000000000000000000000000000000000..608e67d65df859958efa39d0137ec22072301f15 --- /dev/null +++ b/poc/files/mismatches.toml @@ -0,0 +1,14 @@ +[mismatched] +nvim = "neovim" +pkg-config = "pkgconf" +fuse = "fuse-common" +linux-pam = "pam" +"docbook-xml-4.5" = "docbook-xml" +docbook = "docbook-sgml" +xml-parser = "perl-xml-parser" +wheel = "python-wheel" +glib = "glib2" +freetype = "freetype2" +libjpeg = "libjpeg-turbo" +eww = "eww-wayland" +docutils = "python-docutils" diff --git a/poc/src/bin/poc.rs b/poc/src/bin/poc.rs new file mode 100644 index 0000000000000000000000000000000000000000..db134c789c663019e1c9a02c95a269355083cfd1 --- /dev/null +++ b/poc/src/bin/poc.rs @@ -0,0 +1,35 @@ +use anyhow::Result; +use clap::Parser; +use env_logger::Builder; +use log::LevelFilter; + +use poc::{ + cli::{Cli, LogLevel}, + manager::Manager, +}; + +#[tokio::main] +async fn main() -> Result<()> { + let cli = Cli::parse(); + + Builder::new() + .filter_level(>::into(cli.log_level)) + .init(); + + let mut mgr = Manager::new(cli.scan, cli.aur).await?; + if cli.category { + mgr.check_category(&cli.name).await?; + } else { + mgr.check_port(&cli.name, cli.current_version).await?; + } + + if cli.json { + mgr.to_json(&cli.filter.to_string()); + } else if cli.table { + mgr.to_table(&cli.filter.to_string()); + } else { + mgr.to_summary(); + } + + Ok(()) +} diff --git a/poc/src/category.rs b/poc/src/category.rs new file mode 100644 index 0000000000000000000000000000000000000000..e580a487f3f9efe6bda094994d6e710e3ebfcd03 --- /dev/null +++ b/poc/src/category.rs @@ -0,0 +1,127 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use anyhow::{Context, Result}; +use libcport::metadata::Metadata; +use log::{debug, info}; + +use crate::config::PocConfig; + +#[derive(Default)] +pub struct ListOfPorts { + list: Vec, +} + +impl ListOfPorts { + /// Creates a new list of ports by scannnging the category. + /// # Arguments + /// `config` - The configuration to use. + /// `category` - The category to create the list of ports from. + /// # Returns + /// List of ports found in the category. + /// # Errors + /// Returns an error if the list of ports could not be created. + pub fn from_scanner(config: &PocConfig, category: &str) -> Result> { + let mut list = Self::default(); + let dir = config.ports_path.join(format!("ports/{}", category)); + list.scan(&dir)?; + + Ok(list.list) + } + + /// Creates a new list of ports by reading a metadata file. + /// # Arguments + /// `config` - The configuration to use. + /// `category` - The category to create the list of ports from. + /// # Returns + /// List of ports found in the metadata file. + /// # Errors + /// Return an error if the list of ports could not be created. + pub fn from_metadata(config: &PocConfig, category: &str) -> Result> { + let mut list = Self::default(); + let meta = list.read_metadata(&config.path_to_ports_root().join("metadata.toml"))?; + + list.ports_from_metadata(config, &meta, category); + + Ok(list.list) + } + + /// Create list of ports in given category + /// Note: Tokio ReadDir will not return an iterator, use std::fs instead + /// # Arguments + /// `dir_path` - Path to the directory to scan + fn scan(&mut self, dir_path: &PathBuf) -> Result<()> { + info!("Scanning directory {}", dir_path.to_string_lossy()); + self.list = fs::read_dir(dir_path)? + .filter_map(|entry| entry.ok()) + .map(|entry| entry.path()) + // Accept directories only + .filter(|entry| entry.is_dir() && Self::is_port(entry)) + .collect(); + debug!("List of found ports: {:?}", self.list); + Ok(()) + } + + /// Cretae list of ports in given category + /// # Arguments + /// `metadata_path` - Path to the metadata file + /// `category` - Name of the category + /// # Errors + /// Returns an error if the metadata file does not exist or cannot be parsed + fn read_metadata(&mut self, metadata_path: &PathBuf) -> Result { + let metadata: Metadata = toml::from_str( + &fs::read_to_string(metadata_path) + .with_context(|| format!("Failed to read {}", metadata_path.to_string_lossy()))?, + ) + .with_context(|| format!("Failed to deserialize {}", metadata_path.to_string_lossy()))?; + + info!( + "Metadata read from {} successfylly", + metadata_path.to_string_lossy() + ); + + Ok(metadata) + } + + /// Create list of ports in given category + /// # Arguments + /// `config` - The configuration to use. + /// `metadata` - The content of the metadata file. + /// `category` - The category to use + fn ports_from_metadata(&mut self, config: &PocConfig, metadata: &Metadata, category: &str) { + self.list = metadata + .port_sys + .ports + .clone() + .into_iter() + // Convert to PathBuf to be compatible with directory scanner output + .map(|port| config.path_to_port(&port.name)) + // Filter ports by category and accept ports only (scipping directories which not contain a 'port.toml' file) + .filter(|port| Self::in_category(port, category) && Self::is_port(port)) + .collect(); + + debug!("List of ports in the category {} {:?}", category, self.list); + } + + /// Check if the port is belonging to the given category. + /// # Arguments + /// `port` - The port to check. + /// `category` - The category to check. + fn in_category(port: &Path, category: &str) -> bool { + // Get category from the port + let ports_catetegory = port + .parent() + .unwrap_or(Path::new("")) + .to_str() + .unwrap_or_default(); + ports_catetegory.contains(category) + } + + ///Check if given path is a port + fn is_port(dir: &Path) -> bool { + debug!("Checking if {} is a port", dir.to_string_lossy()); + dir.join("port.toml").exists() + } +} diff --git a/poc/src/cli.rs b/poc/src/cli.rs new file mode 100644 index 0000000000000000000000000000000000000000..6658b58c52f7590ee5bc2ea295fa92e5dcbc11e6 --- /dev/null +++ b/poc/src/cli.rs @@ -0,0 +1,108 @@ +use std::fmt::Display; + +use clap::{Parser, ValueEnum}; + +#[derive(Parser, Debug)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + /// Format output as table(s) + #[arg(short, long, group = "grp_output", group = "grp_with_filter")] + pub table: bool, + + /// A short summary + #[arg(short, long, group = "grp_output", group = "grp_filter")] + pub summary: bool, + + /// Output in json format + #[arg(short, long, group = "grp_output", group = "grp_with_filter")] + pub json: bool, + + #[arg( + short, + long, + value_enum, + group = "grp_filter", + requires = "grp_with_filter", + default_value = "all" + )] + pub filter: Filter, + + /// Port category to check for updates + #[arg(short, long, group = "grp_category")] + pub category: bool, + + /// Scan category to check for updates. (Default read from the metadata file) + #[arg(short = 'S', long)] + pub scan: bool, + + /// Check for updates on the AUR server if Arch return an error + #[arg(short, long)] + pub aur: bool, + + /// Enable debug output + #[arg(short, long, value_name = "LEVEL", value_enum, default_value = "off")] + pub log_level: LogLevel, + + /// Port name in format "category/name" or "name" if option "category" is specified + #[arg(value_name = "name", requires = "grp_output")] + pub name: String, + /// Port current version. + #[arg(value_name = "version", group = "grp_category")] + pub current_version: Option, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum Filter { + /// Show all ports + All, + /// Show only success checks + Success, + /// Show only failed checks + Failed, + /// Show outdated ports only + Outdated, +} + +impl Default for Filter { + fn default() -> Self { + Self::All + } +} + +impl Display for Filter { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::All => write!(f, "all"), + Self::Success => write!(f, "success"), + Self::Failed => write!(f, "failed"), + Self::Outdated => write!(f, "outdated"), + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum LogLevel { + Off, + Error, + Warn, + Info, + Debug, +} + +impl Default for LogLevel { + fn default() -> Self { + Self::Off + } +} + +impl From for log::LevelFilter { + fn from(val: LogLevel) -> Self { + match val { + LogLevel::Off => log::LevelFilter::Off, + LogLevel::Error => log::LevelFilter::Error, + LogLevel::Warn => log::LevelFilter::Warn, + LogLevel::Info => log::LevelFilter::Info, + LogLevel::Debug => log::LevelFilter::Debug, + } + } +} diff --git a/poc/src/config.rs b/poc/src/config.rs new file mode 100644 index 0000000000000000000000000000000000000000..984b246fe9d4b387b0ef659ecb163c139e30b731 --- /dev/null +++ b/poc/src/config.rs @@ -0,0 +1,122 @@ +use std::path::PathBuf; + +use anyhow::{anyhow, Context, Result}; +use log::{debug, error, info}; +use serde::{Deserialize, Serialize}; +use tokio::fs; + +#[derive(Debug, Serialize, Deserialize)] +pub struct PocConfig { + pub ports_path: PathBuf, +} + +impl PocConfig { + /// Create a new [`PocConfig`] by load it from the config file if it exists. + /// # Returns + /// # Returns a [`PocConfig`] object + /// # Errors + /// Returns an [`anyhow::Error`] if it fails to create the config. + pub async fn create() -> Result { + match Self::config_from_file().await { + Ok(config) => { + info!("Ports path is: {:?}", config.ports_path); + Ok(config) + } + Err(err) => { + error!("Failed to load config from file: {}", err); + Err(anyhow!("Could not read config file nor environment varable $CALMIRA_PORTS. Please set one of them.")) + } + } + } + + /// Save [`PocConfig`] to the file + /// # Errors + /// Returns an error if the file could not be saved, or if the file could not be created, + /// or if the directory could not be created + pub async fn save(&self) -> Result<()> { + let config_dir = Self::path_to_config()?; + + if !config_dir.is_dir() { + fs::create_dir_all(config_dir.as_path()) + .await + .with_context(|| { + format!( + "Could not create directory '{}'", + config_dir.to_string_lossy() + ) + })?; + } + + let config_file = config_dir.join("config.toml"); + + if !config_file.exists() { + debug!( + "Config {} not exists. Create it.", + config_file.to_string_lossy() + ); + + let toml = toml::to_string(&self).context("Failed to deserialize config")?; + debug!("Config serialized successfully"); + + fs::write(config_file.as_path(), toml) + .await + .with_context(|| format!("Failed to write {}", config_file.to_string_lossy()))?; + } + + Ok(()) + } + + /// Try to create [`PocConfig`] from a file + /// # Returns + /// Returns a [`PocConfig`] if the file could be read + /// # Errors + /// Returns an error if the config file could not be read or if the file could not be parsed + async fn config_from_file() -> Result { + let config_file = Self::path_to_config()?.join("config.toml"); + + let toml = fs::read_to_string(&config_file).await.with_context(|| { + format!( + "Failed to read config file {}", + config_file.to_string_lossy() + ) + })?; + + info!("Loaded config from file: {}", config_file.to_string_lossy()); + + toml::from_str(&toml).context("Failed to deserialize config") + } + + /// Create path to the given port + /// # Argument + /// * `port_name` - The port name in format + /// # Returns + /// Returns the path to the given port + pub fn path_to_port(&self, port_name: &str) -> PathBuf { + self.ports_path.join("ports").join(port_name) + } + + /// Create path to the given category + /// # Argument + /// `category` - name of the category + /// # Returns + /// Returns the path to the given category + pub fn path_to_category(&self, category: &str) -> PathBuf { + self.path_to_ports_root().join(category) + } + + /// Create path to the root directory of the ports + pub fn path_to_ports_root(&self) -> PathBuf { + self.ports_path.join("ports") + } + + /// Create a path to the directory containing the config file + /// # Returns + /// Return path to the config folder + /// # Errors + /// Rerurns an error if the current user's home folder could not be returned + pub fn path_to_config() -> Result { + Ok(home::home_dir() + .context("Failed to get home directory")? + .join(".config/poc")) + } +} diff --git a/poc/src/lib.rs b/poc/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..e8d70fc22cec513143b6de3e52da0dcb47a63992 --- /dev/null +++ b/poc/src/lib.rs @@ -0,0 +1,8 @@ +pub mod category; +pub mod cli; +pub mod config; +pub mod manager; +pub mod mismatches; +pub mod port; +pub mod progress; +pub mod results; diff --git a/poc/src/manager.rs b/poc/src/manager.rs new file mode 100644 index 0000000000000000000000000000000000000000..74b053b7c07819e969918fec088cbb39b227afa5 --- /dev/null +++ b/poc/src/manager.rs @@ -0,0 +1,276 @@ +use std::path::{Path, PathBuf}; + +use anyhow::{anyhow, Result}; +use log::info; + +use crate::{ + category::ListOfPorts, + config::PocConfig, + mismatches::PortMismatches, + port::Port, + progress::Progress, + results::{CheckFailure, CheckResults, CheckSuccess}, +}; + +pub struct Manager { + config: PocConfig, + mismatches: PortMismatches, + scan: bool, + aur: bool, + list_of_ports: Vec, + results: Option, +} + +impl Manager { + /// Creates a new port manager. + /// # Returns + /// Returns a new port [`Manager`] object. + /// Returns + /// Returns an error if could not read the config file or list of mismatches. + pub async fn new(scan: bool, aur: bool) -> Result { + let config = PocConfig::create().await?; + let mismatches = PortMismatches::load().await?; + Ok(Self { + config, + mismatches, + scan, + aur, + list_of_ports: Vec::new(), + results: None, + }) + } + + /// Check one port + /// # Arguments + /// `name` - Port name in format `category/name` + /// `current_version` - Current version of the port + /// # Errors + /// Return error if failrd to read `port.toml` file, or port name is not valid + pub async fn check_port(&mut self, name: &str, current_version: Option) -> Result<()> { + let mut results = CheckResults::new(); + + let mut port = if let Some(current_version) = current_version { + Port::new(name.to_string(), current_version, None) + } else { + // Check if port name contains a category + if !name.contains('/') { + return Err(anyhow!( + "Invalid port name: '{}'. Must be '/{}'", + name, + name + )); + } + + Port::from_toml(&self.config, name.to_string()).await? + }; + + // Add arch_name if it exists in mismatches + port.arch_name = self.mismatches.check(&port.name); + + match port.check(self.aur).await { + Ok(_) => results.add_success(CheckSuccess { + name: port.name, + current_version: port.current_version, + latest_version: port.latest_version, + }), + Err(err) => results.add_failure(CheckFailure::new(&port.name, &err.to_string())), + } + + self.results = Some(results); + + Ok(()) + } + + /// Check all ports in given category + /// # Arguments + /// `category` - Ports category + /// # Errors + /// Returns errr if failed to create list of ports in given category or + /// failed to check ports. + pub async fn check_category(&mut self, category: &str) -> Result<()> { + info!("Prepare list of ports..."); + self.list_of_ports = if self.scan { + ListOfPorts::from_scanner(&self.config, category)? + } else { + ListOfPorts::from_metadata(&self.config, category)? + }; + + info!("Start checking {} ports...", self.list_of_ports.len()); + + self.results = Some(self.check_ports().await?); + Ok(()) + } + + /// Print check results as a table + /// # Arguments + /// `filter` - Filter to apply to the results + pub fn to_table(&self, filter: &str) { + if let Some(results) = self.results.as_ref() { + info!( + "Found check result(s) for {} port(s)", + results.successes.len() + results.failures.len() + ); + results.print_table(filter); + } + } + + /// Print check results as a JSON + /// # Arguments + /// `filter` - Filter to apply to the results + pub fn to_json(&self, filter: &str) { + if let Some(results) = self.results.as_ref() { + info!( + "Found check result(s) for {} port(s)", + results.successes.len() + results.failures.len() + ); + results.print_json(filter); + } + } + + /// Print check results as a short summary + pub fn to_summary(&self) { + if let Some(results) = self.results.as_ref() { + info!( + "Found check result(s) for {} port(s)", + results.successes.len() + results.failures.len() + ); + results.print_summary(); + } + } + + /// Check all ports asynchronously + /// # Returns + /// Returns a [`CheckResults`] object with the results of the check. + /// # Errors + /// Returns error if something went wrong. + async fn check_ports(&mut self) -> Result { + if self.list_of_ports.is_empty() { + return Err(anyhow!("No ports found. Nothing to check.")); + } + + let (success_tx, success_rx) = std::sync::mpsc::channel(); + let (error_tx, error_rx) = std::sync::mpsc::channel(); + let mut handles = Vec::new(); + + let num_threads = self.num_threads(); + info!("Will run requests in {} threads", num_threads); + + let mut progress = Progress::new()?; + + let job_list: Vec> = self + .list_of_ports + .chunks(self.list_of_ports.len() / num_threads) + .map(|s| s.into()) + .collect(); + + for job in &job_list { + progress.add_progressbar(job.len() as u64); + } + + for (i, job) in job_list.iter().enumerate() { + let success_tx_clone = success_tx.clone(); + let error_tx_clone = error_tx.clone(); + let pb = progress.get_progressbar(i)?.clone(); + let job = job.clone(); + let mismatches = self.mismatches.clone(); + let aur = self.aur; + let handle = tokio::spawn(async move { + for port in job.into_iter() { + match Self::check(&port, &mismatches, aur).await { + Ok(success) => { + let result = CheckSuccess { + name: success.name.clone(), + current_version: success.current_version.clone(), + latest_version: success.latest_version.clone(), + }; + success_tx_clone.send(result).unwrap_or_default(); + } + Err(error) => { + error_tx_clone + .send(CheckFailure::new( + port.file_name() + .unwrap_or_default() + .to_str() + .unwrap_or_default(), + &error.to_string(), + )) + .unwrap_or_default(); + } + } + pb.set_message(format!( + "{}", + port.file_name().unwrap_or_default().to_string_lossy() + )); + pb.inc(1); + } + pb.finish_with_message("done"); + }); + handles.push(handle); + } + // All task should already have a copy + drop(success_tx); + drop(error_tx); + + for handle in handles { + handle.await?; + } + + let mut results = CheckResults::new(); + let mut success = false; + let mut failure = false; + while !success || !failure { + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + match success_rx.try_recv() { + Ok(result) => { + results.add_success(result); + } + Err(_) => success = true, + } + match error_rx.try_recv() { + Ok(result) => { + results.add_failure(result); + } + Err(_) => failure = true, + } + } + + Ok(results) + } + + /// Calculate number of threads depending on the number of ports and amount of logical CPU cores + /// # Returns + /// Number of threads + fn num_threads(&self) -> usize { + let num_cpus = num_cpus::get(); + + // Decrease amount of threads to use. + // Sometimes with big amount of ports, and threads > 7 comes out + // strange errors: "Parsing response failed", "No internet connection"... + // Bug in the 'reqwest' library? + if num_cpus <= 2 || self.list_of_ports.len() < 10 { + 1 + } else if self.list_of_ports.len() < 30 { + 2 + } else { + 4 + } + } + + /// Check single port asynchronously. Should be called from separate thread + /// # Arguments + /// `path` - Path to the port + /// # Returns + /// Returns a [`Port`] object + /// # Errors + /// Returns error if path is not a valid port or failed to check port. + async fn check(path: &Path, mismatches: &PortMismatches, aur: bool) -> Result { + let mut port = Port::from_path(&path.join("port.toml")).await?; + + // Add arch_name if it exists in mismatches + port.arch_name = mismatches.check(&port.name); + + port.check(aur).await?; + + Ok(port) + } +} diff --git a/poc/src/mismatches.rs b/poc/src/mismatches.rs new file mode 100644 index 0000000000000000000000000000000000000000..54afd26690f1146c72313072966fe97045cdc189 --- /dev/null +++ b/poc/src/mismatches.rs @@ -0,0 +1,79 @@ +use std::collections::HashMap; + +use anyhow::{Context, Result}; +use log::{debug, error, info}; +use serde::{Deserialize, Serialize}; +use tokio::fs; + +use crate::config::PocConfig; + +const MISMATCHES: &str = "mismatches.toml"; + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct PortMismatches { + mismatched: HashMap, +} + +impl PortMismatches { + /// Loads a list of mismatches from a file + /// # Returns + /// Returns a [`PortMismatches`] object + /// # Errors + /// Returns an error if the file does not exist or cannot be read. + pub async fn load() -> Result { + let config_path = PocConfig::path_to_config()?.join(MISMATCHES); + + let toml = match fs::read_to_string(&config_path).await { + Ok(toml) => toml, + Err(e) => { + error!("Failed to read {}: {}", MISMATCHES, e); + return Ok(Self { + mismatched: HashMap::new(), + }); + } + }; + + info!("Loading mismatches from {}", config_path.display()); + + toml::from_str(&toml).context("Failed to deserialize list of mismatches") + } + + /// Save a list of mismatches to a file + /// # Errors + /// Returns an error if the file could not be written or if it could not be serialized + pub async fn save(&self) -> Result<()> { + let config_path = PocConfig::path_to_config()?.join(MISMATCHES); + + let toml = toml::to_string(&self).context("Failed to serialize mismatches")?; + + fs::write(&config_path, &toml) + .await + .with_context(|| format!("Failed to write {}", config_path.to_string_lossy()))?; + + Ok(()) + } + + /// Returns the mismatched name of the port or `None` if no mismatched name is found + #[must_use] + pub fn check(&self, name: &str) -> Option { + match self.mismatched.get(name) { + Some(m) => { + debug!("Found mismatched name for port '{}': {}", name, m); + Some(m.clone()) + } + None => { + debug!("No mismatched name found for port '{}'", name); + None + } + } + } + + /// Add a new mismatch to the list of mismatches + /// # Arguments + /// `port_name` - The name of the port that was mismatched + /// `search_name` - The name of the package that was searched for a match + pub fn add(&mut self, port_name: &str, search_name: &str) { + self.mismatched + .insert(port_name.to_string(), search_name.to_string()); + } +} diff --git a/poc/src/port.rs b/poc/src/port.rs new file mode 100644 index 0000000000000000000000000000000000000000..6ded758fb906e5782c0b8d6aa17e93bc796ecc79 --- /dev/null +++ b/poc/src/port.rs @@ -0,0 +1,202 @@ +use std::path::Path; + +use anyhow::{anyhow, Context, Result}; +use log::info; +use serde::Serialize; +use serde_json::Value; +use tokio::fs; + +use libcport::port::PortConfig; +use libcport::traits::TomlConfig; + +use crate::config::PocConfig; + +#[derive(Serialize)] +pub struct Port { + pub name: String, + pub arch_name: Option, + pub current_version: String, + pub latest_version: String, +} + +impl Port { + /// Create a new [`Port`] instance + pub fn new(name: String, current_version: String, arch_name: Option) -> Self { + let port_name = if name.contains('/') { + let (_, name) = name.rsplit_once('/').unwrap_or(("", &name)); + name + } else { + &name + }; + + Self { + name: port_name.to_string(), + arch_name, + current_version, + latest_version: String::new(), + } + } + + /// Creates a new [`Port`] instance and fill in fields with the data from the `port.toml` file. + /// # Arguments + /// * `config` - Reference to the [`PocConfig`] struct + /// * `name` - The Port name in format + /// # Errors + /// Returns an error if the file does not exist or cannot be read. + pub async fn from_toml(config: &PocConfig, name: String) -> Result { + let toml_path = config.path_to_port(&name).join("port.toml"); + Self::from_path(&toml_path).await + } + + /// Creates a new [`Port`] instance and fill in fields with the data from the `port.toml` file. + /// # Arguments + /// `path` - The path to the `port.toml` file + /// # Errors + /// Returns an error if the file does not exist or cannot be read. + pub async fn from_path(path: &Path) -> Result { + let toml = fs::read_to_string(path) + .await + .with_context(|| format!("Failed to read {}", path.to_string_lossy()))?; + + // let port_metadata = PortConfig::from_toml(&toml)?; + let port_metadata = PortConfig::from_str(&toml) + .with_context(|| format!("Failed to deserialize {}", path.to_string_lossy()))?; + + info!("Port struct successfully created from port.toml"); + Ok(Self { + name: port_metadata.package.name, + arch_name: None, + current_version: port_metadata.package.version, + latest_version: String::new(), + }) + } + + /// Ask the Arch server for a package and update the latest version + /// # Errors + /// Returns an [`anyhow::Error`] if the package was not found or list of mismatches failed to load/save. + pub async fn check(&mut self, aur: bool) -> Result<()> { + // Determine which name should be used in search + let name = if let Some(name) = &self.arch_name { + info!("Arch name '{}' provided. Use it", name); + name.clone() + } else { + info!("No Arch name provided. Use the port name: '{}'", self.name); + self.name.clone() + }; + + self.latest_version = match Self::ask_arch(&name).await { + Ok(version) => version, + Err(e) => { + if aur { + Self::ask_aur(&name).await? + } else { + return Err(anyhow!( + "Failed to get latest version for package '{}': {}", + name, + e + )); + } + } + }; + + Ok(()) + } + + /// Serializes self to json + /// # Returns + /// Returns a json string of self + /// # Errors + /// Return an error if the json serialization failed + pub fn to_json(&self) -> Result { + Ok(serde_json::to_string(&self)?) + } + + /// Check a package in the Arch official repository + /// # Arguments + /// `name` - The name of the package to check + /// # Returns + /// Package version + async fn ask_arch(name: &String) -> Result { + let pkg = Self::request(&format!( + "https://archlinux.org/packages/search/json/?name={name}" + )) + .await?; + + let pkg_name = pkg["results"][0]["pkgname"].as_str().unwrap_or_default(); + + if pkg_name.is_empty() { + info!("[Arch]: No package found with name '{}'", name); + return Err(anyhow!("No package found with name '{}'", name)); + } + + info!("[Arch]: Package found with name '{}'", pkg_name); + + //Maybe this is unnecessary. "pkg_name" will be empty if the package is not found. + if pkg_name != name { + return Err(anyhow!( + "Package name '{}' does not match '{:?}'", + name, + pkg["results"][0]["pkgname"] + )); + } + + let latest_version = pkg["results"][0]["pkgver"] + .to_string() + .trim_matches('"') + .to_string(); + info!("Latest version: {}", latest_version); + + Ok(latest_version) + } + + /// Check a package in the Aur repository + /// # Arguments + /// `name` - The name of the package to check + /// # Returns + /// Package version + async fn ask_aur(name: &String) -> Result { + let pkg = Self::request(&format!( + "https://aur.archlinux.org/rpc/?v=5&type=info&arg[]={name}" + )) + .await?; + + let pkg_name = pkg["results"][0]["Name"].as_str().unwrap_or_default(); + + if pkg_name.is_empty() { + info!("[Arch]: No package found with name '{}'", name); + return Err(anyhow!("No package found with name '{}'", name)); + } + + info!("[AUR]: Package found with name '{}'", pkg_name); + + //Maybe this is unnecessary. "pkg_name" will be empty if the package is not found. + if pkg_name != name { + return Err(anyhow!( + "Package name '{}' does not match '{:?}'", + name, + pkg["results"][0]["Name"] + )); + } + + let latest_version = pkg["results"][0]["Version"] + .to_string() + .trim_matches('"') + .to_string(); + info!("Latest version: {}", latest_version); + + Ok(latest_version) + } + + /// Make a request to the server and return the response + /// # Arguments + /// `request` - The request to send to the server + /// # Returns + /// Json response from the server + /// # Errors + /// Return error if the request failed, or if the response is not valid json + async fn request(request: &str) -> Result { + let body = reqwest::get(request).await?.text().await?; + + serde_json::from_str(&body).context("Parsing response failed") + } +} diff --git a/poc/src/progress.rs b/poc/src/progress.rs new file mode 100644 index 0000000000000000000000000000000000000000..241094b51bd67e8d2ff5adeabc74140a930b8f9d --- /dev/null +++ b/poc/src/progress.rs @@ -0,0 +1,71 @@ +use std::fmt::Write; + +use anyhow::{anyhow, Result}; +use indicatif::{MultiProgress, ProgressBar, ProgressState, ProgressStyle}; + +pub struct Progress { + main_bar: MultiProgress, + progress_bars: Vec, + style: ProgressStyle, +} + +impl Progress { + /// Creates a new progress barwith default style + /// # Returns + /// Returns a [`Progress`] instance + /// # Errors + /// Returns an error if the progress bar could not be created + pub fn new() -> Result { + Ok( Self { + main_bar: MultiProgress::new(), + style: ProgressStyle::default_bar() + .template("{msg:20} {spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({eta})")? + .with_key("eta", |state: &ProgressState, w: &mut dyn Write| write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()) + .progress_chars("#>-"), + progress_bars: vec![], + }) + } + + /// Adds a new style + /// # Arguments + /// `style` - The new style + pub fn set_style(&mut self, style: ProgressStyle) { + self.style = style; + } + + /// Create a new progress bars + /// # Arguments + /// `num_bars` - The number of progress bars to create + /// `max_progress` - The maximum progress of the progress bars + pub fn init_progressbars(&mut self, num_bars: usize, max_progress: u64) { + for _ in 0..num_bars { + let pb = self.main_bar.add(ProgressBar::new(max_progress)); + pb.set_style(self.style.clone()); + self.progress_bars.push(pb); + } + } + + /// Adds a new progress bar + /// # Arguments + /// `max_progress` - The maximum progress of the progress bar + pub fn add_progressbar(&mut self, max_progress: u64) { + let pb = self.main_bar.add(ProgressBar::new(max_progress)); + pb.set_style(self.style.clone()); + self.progress_bars.push(pb); + } + + /// Return a reference to the progress bar + /// # Arguments + /// `index` - The number of the progress bar + /// # Returns + /// Reference to the progress bar + /// # Errors + /// Returns an error if an index is out of bounds + pub fn get_progressbar(&mut self, index: usize) -> Result<&mut ProgressBar> { + if index < self.progress_bars.len() { + Ok(&mut self.progress_bars[index]) + } else { + Err(anyhow!("index out of range")) + } + } +} diff --git a/poc/src/results.rs b/poc/src/results.rs new file mode 100644 index 0000000000000000000000000000000000000000..1c57c10f4453db8521ff56fb0d032dc268e12e33 --- /dev/null +++ b/poc/src/results.rs @@ -0,0 +1,323 @@ +use console::style; +use log::{debug, info}; +use serde::{Deserialize, Serialize}; +use tabled::{builder::Builder, col, settings::Style, Tabled}; + +pub trait CheckResult { + fn to_record(&self) -> Vec; +} + +#[derive(Debug, Tabled, Deserialize, Serialize, Default, Clone)] +pub struct CheckFailure { + pub name: String, + pub error: String, +} + +impl CheckResult for CheckFailure { + fn to_record(&self) -> Vec { + vec![ + format!("{}", style(self.name.clone()).cyan()), + format!("{}", style(self.error.clone()).red()), + ] + } +} + +impl CheckFailure { + pub fn new(name: &str, error: &str) -> Self { + Self { + name: name.to_string(), + error: error.to_string(), + } + } +} + +#[derive(Debug, Tabled, Deserialize, Serialize, Default, Clone)] +pub struct CheckSuccess { + pub name: String, + pub current_version: String, + pub latest_version: String, +} + +impl CheckResult for CheckSuccess { + fn to_record(&self) -> Vec { + if self.current_version == self.latest_version { + vec![ + format!("{}", style(self.name.clone()).cyan()), + format!("{}", style(self.current_version.clone()).green()), + format!("{}", style(self.latest_version.clone()).green()), + ] + } else { + vec![ + format!("{}", style(self.name.clone()).cyan()), + format!("{}", style(self.current_version.clone()).red()), + format!("{}", style(self.latest_version.clone()).magenta()), + ] + } + } +} + +#[derive(Debug, Serialize)] +pub struct CheckResults { + pub failures: Vec, + pub successes: Vec, +} + +impl CheckResults { + pub fn new() -> Self { + Self { + failures: vec![], + successes: vec![], + } + } + + /// Add a failure result + /// # Arguments + /// `failure` - The failure to add + pub fn add_failure(&mut self, failure: CheckFailure) { + info!("Added package as a failure: '{}'", failure.name); + self.failures.push(failure); + } + + /// Add a success result + /// # Arguments + /// `success` - The success to add + pub fn add_success(&mut self, success: CheckSuccess) { + info!("Added package '{}' as a success", success.name); + self.successes.push(success); + } + + /// Print a summary of the check results + pub fn print_summary(&self) { + println!( + "Checked {} ports", + style(self.failures.len() + self.successes.len()).blue() + ); + println!("Successes: {}", style(self.successes.len()).green()); + println!("Failures: {}", style(self.failures.len()).yellow()); + println!("------------------------------------------------"); + println!( + "{} ports are up to date", + style(self.count_up_to_date()).green() + ); + println!( + "{} ports need to be upgraded", + style(self.count_outdate()).red() + ); + } + + /// Print a table of the check results + /// # Arguments + /// `filter` - The filter to apply to the table + pub fn print_table(&self, filter: &str) { + match filter { + "all" => { + let success = self.success_to_table(); + let failure = self.failure_to_table(); + + if success.is_some() && failure.is_some() { + println!( + "{}", + col![success.unwrap_or_default(), failure.unwrap_or_default()] + ); + } else if let Some(success) = success { + println!("{}", success); + } else if let Some(failure) = failure { + println!("{}", failure); + } else { + println!("No results found"); + } + } + "failed" => { + if let Some(failure) = self.failure_to_table() { + println!("{}", failure); + } else { + println!("No failures found"); + } + } + "success" => { + if let Some(success) = self.success_to_table() { + println!("{}", success); + } else { + println!("No successfully checked ports"); + } + } + "outdated" => println!("{}", self.outdated_to_table()), + _ => {} + } + } + + /// Print a JSON representation of the check results + /// # Arguments + /// `filter` - The filter to apply to the table + pub fn print_json(&self, filter: &str) { + match filter { + "all" => println!("{}", self.all_to_json()), + "failed" => println!("{}", self.failure_to_json()), + "success" => println!("{}", self.success_to_json()), + "outdated" => println!("{}", self.outdated_to_json()), + _ => {} + } + } + + /// Convert the check results, including failures and successes, to a JSON representation + /// # Returns + /// Returns a JSON representation + fn all_to_json(&self) -> String { + match serde_json::to_string(&self) { + Ok(json) => json, + Err(e) => { + eprintln!("Failed to convert to JSON: {}", e); + String::new() + } + } + } + + /// Convert the success check results to a table + /// # Returns + /// Returns a table representation + fn success_to_table(&self) -> Option { + if self.successes.is_empty() { + return None; + } + + let mut builder = Builder::new(); + + builder.set_header(self.table_header_success()); + for item in self.successes.iter() { + builder.push_record(item.to_record()); + } + + Some(builder.build().with(Style::modern()).to_string()) + } + + /// Convert the failure check results to a JSON representation + /// # Returns + /// Returns a JSON representation + fn success_to_json(&self) -> String { + match serde_json::to_string(&self.successes) { + Ok(json) => json, + Err(e) => { + eprintln!("Failed to serialize json: {}", e); + String::new() + } + } + } + + /// Convert the failure check results to a table + /// # Returns + /// Returns a table representation + fn failure_to_table(&self) -> Option { + if self.failures.is_empty() { + debug!("No failures found."); + return None; + } + + let mut builder = Builder::new(); + + builder.set_header(self.table_header_failure()); + for item in self.failures.iter() { + builder.push_record(item.to_record()); + } + + Some(builder.build().with(Style::modern()).to_string()) + } + + /// Convert the failure check results to a JSON representation + /// # Returns + /// Returns a JSON representation + fn failure_to_json(&self) -> String { + match serde_json::to_string(&self.failures) { + Ok(json) => json, + Err(e) => { + eprintln!("Failed to serialize json: {}", e); + String::new() + } + } + } + + /// Filter the check results to only include outdated packages and return a table + /// # Returns + /// Returns a table representation + fn outdated_to_table(&self) -> String { + let mut builder = Builder::new(); + + builder.set_header(self.table_header_success()); + for item in self.successes.iter() { + if Self::is_outdated(item) { + builder.push_record(item.to_record()); + } + } + + builder.build().with(Style::modern()).to_string() + } + + /// Filter the check results to only include outdated packages and return a JSON representation + /// # Returns + /// Returns a JSON representation + fn outdated_to_json(&self) -> String { + let mut outdated = Vec::new(); + for item in self.successes.iter() { + if Self::is_outdated(item) { + outdated.push(item.clone()); + } + } + match serde_json::to_string(&outdated) { + Ok(json) => json, + Err(e) => { + eprintln!("Failed to serialize json: {}", e); + String::new() + } + } + } + + /// Create a table header for the success check results + /// # Returns + /// Returns an header as a vector of strings + fn table_header_success(&self) -> Vec { + vec![ + "Name".to_string(), + "Current Version".to_string(), + "Latest Version".to_string(), + ] + } + + /// Create a table header for the failure check results + /// # Returns + /// Returns an header as a vector of strings + fn table_header_failure(&self) -> Vec { + vec!["Name".to_string(), "Error".to_string()] + } + + /// Check if a package is outdated + fn is_outdated(port: &CheckSuccess) -> bool { + port.current_version != port.latest_version + } + + /// Count the number of up to date packages + /// # Returns + /// Amount of up to date packages + fn count_up_to_date(&self) -> usize { + self.successes + .iter() + .filter(|port| !Self::is_outdated(port)) + .collect::>() + .len() + } + + /// Count the number of outdated packages + /// # Returns + /// Amount of outdate packages + fn count_outdate(&self) -> usize { + self.successes + .iter() + .filter(|port| Self::is_outdated(port)) + .collect::>() + .len() + } +} + +impl Default for CheckResults { + fn default() -> Self { + Self::new() + } +} diff --git a/pu/Cargo.toml b/pu/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..29747272e611b37d26270c43ea84cef36f108b94 --- /dev/null +++ b/pu/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "pu" +version = "0.3.0" +edition = "2021" +description = "The simple tool to help update ports for the Calmira Linux." + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[[bin]] +name = "pu" + +[dependencies] +libportutil = { version = "0.1", path = "../libportutil" } +libcport = { version = "2", path = "../libcport" } +git2 = "0.18" + +libmsg.workspace = true +clap.workspace = true +chrono.workspace = true +thiserror.workspace = true +anyhow.workspace = true +walkdir.workspace = true +num_cpus.workspace = true +which.workspace = true +colored.workspace = true +tokio.workspace = true +indicatif.workspace = true diff --git a/pu/Makefile b/pu/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..13730c2b61c2fa10209bbbb85c401d9c97af8db1 --- /dev/null +++ b/pu/Makefile @@ -0,0 +1,12 @@ +build: + cargo build + +build.release: + cargo build --release + +install: + cp ../target/release/pu ~/.local/bin/pu + cp scripts/port_build.sh ~/.local/bin/port_build.sh + +clean: + cargo clean \ No newline at end of file diff --git a/pu/README.md b/pu/README.md new file mode 100644 index 0000000000000000000000000000000000000000..089de57ff0b358f95772481c1a5eb035f1b6f1fd --- /dev/null +++ b/pu/README.md @@ -0,0 +1,178 @@ +# PortUtil + +`pu` is the tool for updating existing ports. + +## Requirements +The program required variables `CALMIRA_PORT` and `CALMIRA_WORK` to be available in the environment. + +The variable `CALMIRA_PORT` should contain the path to the `Ports` repository. +The variable `CALMIRA_WORK` should contain the path to the working directory where the `pu` will create the necessary files. + +### Example +#### Fish +```Fish +set -Ux CALMIRA_WORK +set -Ux CALMIRA_PORT + +``` +#### Bash +```Bash +export CALMIRA_WORK= +export CALMIRA_PORT= + +``` + +## Theory +The `pu` will create a new directory inside the `CALMIRA_WORK` directory with the name of the port. This directory will be used as the root for the port creation. +The directory structure will be looked like the following: +```bash + + └── - This will be root for the port + └── + ├── - - Extracted soirce code + ├── archive.tar.Xz - Downloaded archive from the given url + ├── .ron - Metadata about the port + ├── install - The installation instructions + └── port_build.sh - The helper script to build the port +``` + +## Usage +The `pu` has different commands to help update a port which can be shown by using the `--help` flag. + +```Bash +❯ pu --help +The simple tool to help update ports for Calmira Linux. + +Usage: pu [OPTIONS] + +Commands: + gen Create port files 'README.md', 'files.list' + get Download archive and prepare nessessary files + update Update an existing port + run Execute functons from the `install` file + help Print this message or the help of the given subcommand(s) + +Options: + -c, --container + -h, --help Print help + -V, --version Print version +``` + +### Get +The `get` command will prepare all necessary files. See the [Teory](#theory) for detailed information. + +```bash +❯ pu get --help +Download archive and prepare nessessary files + +Usage: pu get + +Arguments: + Port name in format 'category/name' + Url path to an archive + +Options: + -h, --help Print help +``` + +### Update +The `update` command can be used to update an existing port to a new version. + +```bash +❯ pu update --help +Update existing port from files prepared by 'get' command + +Usage: pu update [URL] + +Arguments: + Port name in format 'category/name'. Ex. 'base/bc' + Verion update to + [URL] Url path to an archive + +Options: + -h, --help Print help +``` + +It can be used in two ways: +1. If a URL is provided the `update` will try to update the port automatically. Files in the `Ports/ports//` directory will be updated at the end of the workflow. +2. If the `update` doesn't have a URL specified it will use the files from the `CALMIRA_WORK/port_name/src/` directory to update the port. +It assumes that the user calls the `get` command first. +This mode can be useful for port which have errors during build or installation and require manual editing of some files. + +### Gen +The `gen` command is used to generate templates for port files. + +```bash +❯ pu gen --help +Create port files 'README.md', 'files.list' + +Usage: pu gen + +Commands: + files Prepare the 'files.list' file + readme Generate README.md file from 'port.toml' + help Print this message or the help of the given subcommand(s) + +Options: + -h, --help Print help +``` + +#### files +The `files` subcommand is used to generate the `files.list` file. + +```bash +❯ pu gen files --help +Prepare the 'files.list' file + +Usage: pu gen files [DESTINATION] + +Arguments: + Path to the working directory + [DESTINATION] Path to the destination folder + +Options: + -h, --help Print help +``` +The `SOURCE` argument is the path to the directory where the new port is installed. +The `DESTINATION` argument is the path to the destination folder. It is mostly used to place the generated file into the port directory inside the `Ports`. +The generated file `files.list` will be placed in the current directory if `DESTINATION` is not specified. + +#### readme +The `readme` command will generate the `README.md` file from the `port.toml` file located in the `Ports/ports//` directory. + +```bash +❯ pu gen readme --help +Generate README.md file from 'port.toml' + +Usage: pu gen readme + +Arguments: + Port name in format 'category/name'. Ex. 'base/bc' + +Options: + -h, --help Print help +``` + +### Run +The `run` command is used to run functions from the `install` file to be able to create a new port. + +The `run` will execute following functions sequentially if argument `all` is provided: +1. `prepare()` +2. `build()` +3. `preinst()` +4. `install()` +5. `postinst()` + +```bash +❯ pu run --help +Execute functons from an `install` file + +Usage: pu run [FUNCTION] + +Arguments: + Port name in format 'category/name'. Ex. 'base/bc' + [FUNCTION] Function name to execute. Empty value execute all functions [possible values: all, prepare, build, preinst, install, postinst] + +Options: + -h, --help Print help +``` \ No newline at end of file diff --git a/pu/scripts/port_build.sh b/pu/scripts/port_build.sh new file mode 100755 index 0000000000000000000000000000000000000000..eff3317d49caa0c9a322ba83489e29b8ff1de979 --- /dev/null +++ b/pu/scripts/port_build.sh @@ -0,0 +1,59 @@ +#!/bin/bash -e + +. $WORK_DIR/install + +export MAKEFLAGS="-j${JOBS}" +export NINJAJOBS="${JOBS}" +export DESTDIR="${CPORT_INSTALLATION_PREFIX}" + +check_status() { + CODE=$1 + echo "Checking code: $CODE" + if [ $CODE != 0 ]; then + echo "Error with '$2': $CODE code returned!" + exit $CODE + fi +} + +if [ -z "$BUILD_DIR" ]; then + cd ${WORK_DIR}/${NAME}-${VERSION} +elif [ "$BUILD_DIR" = "none" ]; then + cd ${WORK_DIR} +else + cd ${WORK_DIR}/${BUILD_DIR} +fi +check_status $? "cd" + +echo "Entring into $PWD" + +if [[ ( "$RUN" == "prepare" || "$RUN" == "all") && "$(type -t 'prepare')" == "function" ]]; then + echo "Executing the 'prepare()' function..." + prepare + check_status $? "prepare()" +fi + +if [[ ( "$RUN" == "build" || "$RUN" == "all") && "$(type -t 'build')" == "function" ]]; then + echo "Executing the 'build()' function..." + build + check_status $? "build()" +fi + +if [[ ( "$RUN" == "preinst" || "$RUN" == "all") && "$(type -t 'preinst')" == "function" ]]; then + echo "Executing the 'preinst()' function..." + preinst + check_status $? "preinst()" +fi + +if [[ ( "$RUN" == "pkginst" || "$RUN" == "all") && "$(type -t 'pkginst')" == "function" ]]; then + echo "Executing the 'pkginst()' function..." + pkginst + check_status $? "pkginst()" +fi + +if [[ ( "$RUN" == "postinst" || "$RUN" == "all") && "$(type -t 'postinst')" == "function" ]]; then + echo "Executing the 'postinst()' function..." + postinst + check_status $? "postinst()" +fi + +exit 0 \ No newline at end of file diff --git a/pu/src/bin/pu.rs b/pu/src/bin/pu.rs new file mode 100644 index 0000000000000000000000000000000000000000..05d6abc99df6cbcc87d3a91899201b32f6752c7d --- /dev/null +++ b/pu/src/bin/pu.rs @@ -0,0 +1,76 @@ +use anyhow::Result; +use clap::Parser; + +use pu::{ + cli::{Cli, Command, SubCmd}, + commands::{gen, get, run, update}, + story::Story, +}; + +#[tokio::main] +async fn main() -> Result<()> { + let args = Cli::parse(); + + match args.command { + Command::Gen { subcommand } => match subcommand { + SubCmd::Files { + source, + destination, + } => { + let _ = gen::files_list(&source, destination, &mut Story::new(0, "FILES")).await?; + } + SubCmd::Readme { name, destination } => { + gen::readme(&name, destination, None, &mut Story::new(0, "README")).await? + } + SubCmd::Port { + git, + name, + destination, + } => gen::port_toml(&name, destination, git, &mut Story::new(0, "PORT")).await?, + SubCmd::Install { + git, + name, + destination, + build, + } => { + gen::install_template( + &name, + destination, + git, + &build.unwrap_or_default().to_string(), + &mut Story::new(0, "INSTALL"), + ) + .await? + } + }, + Command::Get { name, path } => { + let _package = get::get(&path, &name, &mut Story::new(0, "GET")).await?; + } + Command::Run { + show_output, + name, + function, + } => match function { + Some(func) => { + let _sbu = run::run( + &name, + &func.to_string(), + show_output, + &mut Story::new(0, &func.to_string().to_uppercase()), + ) + .await?; + } + None => { + let _sbu = run::run(&name, "all", show_output, &mut Story::new(0, "BUILD")).await?; + } + }, + Command::Update { + no_cleanup, + name, + version, + url, + } => update::update(args.container, &name, &version, &url, no_cleanup).await?, + } + + Ok(()) +} diff --git a/pu/src/cli.rs b/pu/src/cli.rs new file mode 100644 index 0000000000000000000000000000000000000000..a56d6dd6b66c41d8cd1eea584c0930ed1763509f --- /dev/null +++ b/pu/src/cli.rs @@ -0,0 +1,139 @@ +use std::fmt::Display; + +use clap::{Parser, Subcommand, ValueEnum}; + +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +pub struct Cli { + /// Optional command to operate on + #[command(subcommand)] + pub command: Command, + + #[arg(short, long)] + pub container: bool, +} + +#[derive(Subcommand)] +pub enum Command { + /// Create port files 'README.md', 'files.list'. + Gen { + /// Command to generate files + #[command(subcommand)] + subcommand: SubCmd, + }, + /// Download archive and prepare nessessary files + Get { + /// Port name in format 'category/name'. + name: String, + /// Url path to an archive + path: String, + }, + /// Update an existing port + Update { + /// Do not cleanup working directory after update + #[arg(long, value_name = "no-cleanup")] + no_cleanup: bool, + /// Port name in format 'category/name'. Ex. 'base/bc' + name: String, + /// Verion update to + version: String, + /// Url path to an archive. + url: Option, + }, + /// Execute functons from the `install` file + Run { + #[arg(short, long, value_name = "show-output")] + show_output: bool, + /// Port name in format 'category/name'. Ex. 'base/bc' + name: String, + /// Function name to execute. Empty value execute all functions. + #[arg(value_enum)] + function: Option, + }, +} + +#[derive(Subcommand)] +pub enum SubCmd { + /// Prepare the 'files.list' file + Files { + /// Path to the working directory + source: String, + /// Path to the destination folder. Default current directory + destination: Option, + }, + /// Generate README.md file from 'port.toml' + Readme { + /// Port name in format 'category/name'. Ex. 'base/bc' + name: String, + /// Path to the destination folder. Default current directory + destination: Option, + }, + /// Generate a Port.toml template file + Port { + /// Fill the maintainer from the git config + #[arg(short, long)] + git: bool, + /// Port name in format 'category/name'. Ex. 'base/bc' + name: String, + /// Path to the destination folder. Default create file in the port directory + destination: Option, + }, + /// Generate an intall template file + Install { + #[arg(short, long)] + git: bool, + /// Build system. Default crate the make build system + #[arg(short, long)] + build: Option, + /// Port name in format 'category/name'. Ex. 'base/bc' + name: String, + /// Path to the destination folder. Default create file in the port directory + destination: Option, + }, +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum BuildFunctions { + All, + Prepare, + Build, + Preinst, + Pkginst, + Postinst, +} + +impl Display for BuildFunctions { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::All => write!(f, "all"), + Self::Prepare => write!(f, "prepare"), + Self::Build => write!(f, "build"), + Self::Preinst => write!(f, "preinst"), + Self::Pkginst => write!(f, "pkginst"), + Self::Postinst => write!(f, "postinst"), + } + } +} + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)] +pub enum BuildSystem { + Make, + Meson, + Cargo, +} + +impl Default for BuildSystem { + fn default() -> Self { + Self::Make + } +} + +impl Display for BuildSystem { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Meson => write!(f, "meson"), + Self::Cargo => write!(f, "cargo"), + _ => write!(f, "make"), + } + } +} diff --git a/pu/src/commands.rs b/pu/src/commands.rs new file mode 100644 index 0000000000000000000000000000000000000000..f4ba25a122d01f833120af87673a00603ce7267b --- /dev/null +++ b/pu/src/commands.rs @@ -0,0 +1,4 @@ +pub mod gen; +pub mod get; +pub mod run; +pub mod update; diff --git a/pu/src/commands/gen.rs b/pu/src/commands/gen.rs new file mode 100644 index 0000000000000000000000000000000000000000..8ec1e7555d86a7766b9b8c288a938631ab7048d7 --- /dev/null +++ b/pu/src/commands/gen.rs @@ -0,0 +1,378 @@ +use chrono::Datelike; +use std::path::{Path, PathBuf}; + +use anyhow::{anyhow, Result}; +use colored::Colorize; +use libcport::{port::PortConfig, traits::TomlConfig}; +use libportutil::{get_port_dir, get_port_name}; +use tokio::fs; +use walkdir::WalkDir; + +use crate::{ + git::get_maintainer, + story::{MsgType, Story, Target}, +}; + +pub async fn install_template( + port_full_name: &str, + destination: Option, + use_git: bool, + bs: &str, + story: &mut Story, +) -> Result<()> { + story.begin(&format!( + "Generate file '{}'", + Target::File("install".to_string()) + )); + + let install_path = check_dir(destination, port_full_name) + .await? + .join("install"); + + gen_install( + install_path.as_path(), + get_port_name(port_full_name)?.as_str(), + use_git, + bs, + ) + .await?; + + story.end( + &format!( + "Created '{}'", + Target::File(install_path.to_str().unwrap_or_default().to_string()) + ), + &MsgType::Ok, + ); + Ok(()) +} + +pub async fn port_toml( + port_full_name: &str, + destination: Option, + use_git: bool, + story: &mut Story, +) -> Result<()> { + story.begin(&format!( + "Generate file '{}'", + Target::File("port.toml".to_string()) + )); + + let toml_path = check_dir(destination, port_full_name) + .await? + .join("port.toml"); + + gen_port_toml( + toml_path.as_path(), + get_port_name(port_full_name)?.as_str(), + use_git, + ) + .await?; + + story.end( + &format!( + "Created '{}'", + Target::File(toml_path.to_str().unwrap_or_default().to_string()) + ), + &MsgType::Ok, + ); + + Ok(()) +} + +pub async fn files_list( + source_dir: &str, + destination: Option, + story: &mut Story, +) -> Result { + story.begin(&format!( + "Generate file '{}'", + Target::File("files.list".to_string()) + )); + + story.line(&format!( + "Scanning directodies in the '{}'", + Target::Dir(source_dir.to_string()) + )); + + let (list, size) = scan_dir(source_dir)?; + + let file = if let Some(dest) = destination { + format!("{}/files.list", dest.trim_end_matches('/')) + } else { + let current_dir = std::env::current_dir().map_err(|err| { + anyhow!( + "{} to get current directory path {}", + "failed".red(), + err.to_string().red() + ) + })?; + format!("{}/files.list", current_dir.to_string_lossy()) + }; + + fs::write(&file, list.join("\n")).await.map_err(|err| { + anyhow!( + "{} to create {} cause {}", + "failed".red(), + file.green(), + err.to_string().red() + ) + })?; + + story.end(&format!("Created '{}'", Target::File(file)), &MsgType::Ok); + + Ok(size) +} + +pub async fn readme( + port_full_name: &str, + destination: Option, + port: Option, + story: &mut Story, +) -> Result<()> { + story.begin(&format!( + "Generate file '{}'", + Target::File("README.md".to_string()) + )); + + let port_dir = get_port_dir(port_full_name)?; + + let port_meta = if let Some(p) = port { + p + } else { + let file = format!("{port_dir}/port.toml"); + let toml = fs::read_to_string(&file).await.map_err(|err| { + anyhow!( + "{} to read {} cause {}", + "failed".red(), + file.clone().green(), + err.to_string().red() + ) + })?; + if let Ok(port) = PortConfig::from_str(&toml) { + story.line(&format!( + "Generate info from the '{}'", + Target::File(file.clone()) + )); + + port + } else { + story.line_with_type( + &format!("Failed to find '{}'", Target::File(file)), + &MsgType::Warning, + ); + + story.line(&format!( + "Create template '{}'", + Target::File("README.md".to_string()) + )); + + PortConfig::default() + } + }; + + let readme_file = if let Some(dest) = destination { + format!("{}/README.md", dest.trim_end_matches('/')) + } else { + let current_dir = std::env::current_dir().map_err(|err| { + anyhow!( + "{} to get current directory path {}", + "failed".red(), + err.to_string().red() + ) + })?; + format!("{}/README.md", current_dir.to_string_lossy()) + }; + + fs::write(&readme_file, port_meta.get_markdown_string()) + .await + .map_err(|err| { + anyhow!( + "{} to create {} cause: {}", + "failed".red(), + readme_file.green(), + err.to_string().red() + ) + })?; + + story.end( + &format!("Created '{}'", Target::File(readme_file)), + &MsgType::Ok, + ); + + Ok(()) +} + +// TODO: Move to the 'async_walkdir +fn scan_dir(root_dir: &str) -> Result<(Vec, f64)> { + let mut files_summary_size = 0; + // Be sure that no '/' at the end. Will be used in filter later. + let root_dir = root_dir.trim_end_matches('/'); + if !Path::new(root_dir).exists() { + return Err(anyhow!(format!( + "{} to create {}{}. Files are not prepared. Please compile and install port into the {}", + "failed".red(), + root_dir.green(), + "/files.list".green(), + root_dir.blue()))); + } + let mut files_list: Vec = Vec::new(); + for entry in WalkDir::new(root_dir).into_iter().filter_map(|e| e.ok()) { + if !entry.path().is_dir() { + let file = entry.path().to_str().ok_or(anyhow!(format!( + "{} to read file '{}'", + "failed".red(), + entry.path().display() + )))?; + let meta = entry.metadata()?; + + // Exclude source files + if !file.contains(&format!("{root_dir}/src")) { + // Calculate as KB + files_summary_size += meta.len() / 1024; + files_list.push(file.replace(&format!("{root_dir}/"), "/")); + } + } + } + + Ok((files_list, files_summary_size as f64 / 1024.)) +} + +async fn check_dir(destination: Option, port_full_name: &str) -> Result { + let destination_dir = if let Some(dest) = destination { + Path::new(&dest).canonicalize()? + } else { + PathBuf::from(get_port_dir(port_full_name)?) + }; + + // FIXME: Use 'libportutil/lib.rs/create_dir()' after fixing issue 30 + if !destination_dir.exists() { + fs::create_dir_all(destination_dir.as_path()).await?; + } + + Ok(destination_dir) +} + +async fn gen_port_toml(path: &Path, name: &str, maintainer: bool) -> Result<()> { + let mut port = PortConfig::default(); + + port.package.name = name.to_string(); + let maintainer = if maintainer { get_maintainer() } else { None }; + + if let Some(maintainer) = maintainer { + port.package.maintainers.push(maintainer); + }; + + fs::write(path, port.to_string()?).await?; + Ok(()) +} + +async fn gen_install(path: &Path, name: &str, maintainer: bool, build_system: &str) -> Result<()> { + let mut template = String::new(); + + template.push_str(&install_header(name, maintainer)); + template.push_str(&install_environment()); + template.push_str(&install_local_variables(name)); + template.push_str(&install_bs(build_system)); + + fs::write(path, template).await?; + + Ok(()) +} + +fn install_header(name: &str, maintainer: bool) -> String { + let mut header = String::new(); + let maintainer = if maintainer { get_maintainer() } else { None }; + let current_date = chrono::Utc::now(); + + header.push_str("#!/bin/bash -e\n"); + header.push_str(&format!("# Build script for '{name}' package\n")); + header.push_str(&format!( + "# Copyright (C) 2021 - {} {}\n\n", + current_date.year(), + maintainer.unwrap_or_default() + )); + header +} + +fn install_environment() -> String { + let mut environment = String::new(); + + environment.push_str(&format!("{:#<79}\n", '#')); + environment.push_str("## ENVIRONMENT VARIABLES:\n##\n\n"); + environment.push_str("# - 'CPORT_BUILD_THREADS' - число потоков сборки\n"); + environment.push_str("# - 'CPORT_INSTALLATION_PREFIX' - префикс установки порта\n"); + environment.push_str("# - 'SRC_DIR' - путь до исходного кода порта\n"); + environment + .push_str("# - 'PORT_DIR' - путь до каталога с файлами порта ('install', 'port.toml',\n# etc.)\n"); + environment.push_str("# - 'IS_TEST' - тестировать ли порт после сборки\n"); + environment.push_str("# - 'MAKEFLAGS' - указывает число потоков сборки для GNU Make\n"); + environment.push_str("# - 'NINJAJOBS' - указывает число потоков сборки для ninja\n"); + environment.push_str("# - 'LOG_FILE' - путь до файла логов cport\n\n\n"); + + environment +} + +fn install_local_variables(name: &str) -> String { + let mut vars = String::new(); + + vars.push_str(&format!("{:#<79}\n", '#')); + vars.push_str("## BUILD INSTRUCTIONS:\n##\n\n"); + vars.push_str(&format!("NAME=\"{name}\"\n")); + vars.push_str("VERSION=\"\"\n"); + vars.push_str("#ARCHIVE=\"${NAME}-${VERSION}.tar.gz\" # custom package archive name\n"); + vars.push_str("#BUILD_DIR=\"\" # custom build dir name\n\n"); + + vars +} + +fn install_bs(build_system: &str) -> String { + let mut bs = String::new(); + let mut prepare = String::new(); + let mut build = String::new(); + let mut pkginst = String::new(); + + match build_system { + "meson" => { + prepare.push_str("\tmeson setup build --prefix=/usr --buildtype=release"); + build.push_str("\tninja -C build"); + pkginst.push_str("\tninja -C build install"); + } + "cargo" => { + prepare.push_str("\t:"); + build.push_str("\tcargo build --release"); + pkginst.push_str("\t:"); + } + _ => { + prepare.push_str("\t./configure --prefix=/usr"); + build.push_str("\tmake"); + pkginst.push_str("\tmake install"); + } + } + + bs.push_str("function prepare() {\n"); + bs.push_str(&prepare); + bs.push_str("\n}\n\n"); + + bs.push_str("function build() {\n"); + bs.push_str(&build); + bs.push_str("\n}\n\n"); + + bs.push_str("function test() {\n"); + bs.push_str("\t:"); + bs.push_str("\n}\n\n"); + + bs.push_str("function preinst() {\n"); + bs.push_str("\t:"); + bs.push_str("\n}\n\n"); + + bs.push_str("function pkginst() {\n"); + bs.push_str(&pkginst); + bs.push_str("\n}\n\n"); + + bs.push_str("function postinst() {\n"); + bs.push_str("\t:"); + bs.push_str("\n}\n\n"); + + bs +} diff --git a/pu/src/commands/get.rs b/pu/src/commands/get.rs new file mode 100644 index 0000000000000000000000000000000000000000..2f84f5fb938b8b2751821f0db8f929803dbd6794 --- /dev/null +++ b/pu/src/commands/get.rs @@ -0,0 +1,140 @@ +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use colored::Colorize; +use libportutil::{archive::Archive, get_port_dir}; +use tokio::fs; + +use crate::{ + progress::SpinnerBar, + story::{MsgType, Story, Target}, +}; + +pub async fn get(url: &str, port_full_name: &str, story: &mut Story) -> Result { + let mut package = Archive::new(url, port_full_name)?; + + story.begin(&format!( + "Preparing working directory: '{}'", + Target::Dir(package.port_work_dir.clone()) + )); + + download_with_spinner(&package).await?; + + (story.line(&format!( + "Downloaded '{}'", + Target::Url(package.url.clone()) + ))); + + extract_with_spinner(&package).await?; + + story.line(&format!( + "Extracting '{}' completed successfully", + Target::File(package.file_name.clone()) + )); + + story.line("Calculate hashes"); + + package.calculate_hashes().await?; + + let file = format!("{}/{}.ron", package.port_work_dir, package.package_name); + + story.line(&format!( + "Save metadata to the '{}'", + Target::File(file.clone()) + )); + + fs::write(file, package.to_ron()?).await?; + + story.line(&format!( + "Copy helper script '{}'", + Target::File("port_build.sh".to_string()) + )); + + // TODO: TBD file location. + let port_build = which::which("port_build.sh") + .with_context(|| format!("Failed to find '{}'", "port_build.sh".green()))?; + + package.copy(port_build).await?; + + story.line(&format!( + "Copy the '{}' script to '{}'", + Target::File("install".to_string()), + Target::Dir(package.port_work_dir.clone()), + )); + + let install_file = PathBuf::from(get_port_dir(port_full_name)?).join("install"); + match package.copy(install_file.clone()).await { + Ok(()) => (), + Err(_) => story.line_with_type( + &format!( + "[{}] Failed to copy '{}'", + "ERROR".red(), + Target::File(install_file.to_string_lossy().to_string()) + ), + &MsgType::Warning, + ), + } + + story.line(&format!( + "Copy the '{}' script to '{}'", + Target::File("port.toml".to_string()), + Target::Dir(package.port_work_dir.clone()), + )); + + let port_file = PathBuf::from(get_port_dir(port_full_name)?).join("port.toml"); + match package.copy(port_file.clone()).await { + Ok(()) => (), + Err(_) => story.line_with_type( + &format!( + "[{}] Failed to copy '{}'", + "ERROR".red(), + Target::File(port_file.to_string_lossy().to_string()) + ), + &MsgType::Warning, + ), + } + + story.end( + &format!( + "All files prepared here: '{}'", + Target::Dir(package.port_work_dir.clone()) + ), + &MsgType::Ok, + ); + + Ok(package) +} + +async fn download_with_spinner(package: &Archive) -> Result<()> { + let mut spinner = SpinnerBar::new()?; + + spinner.set_message(format!( + "Downloading '{}'...", + Target::Url(package.url.clone()) + )); + + spinner.start(); + + package.download().await?; + + spinner.finish(); + + Ok(()) +} + +async fn extract_with_spinner(package: &Archive) -> Result<()> { + let mut spinner = SpinnerBar::new()?; + + spinner.set_message(format!( + "Extracting archive '{}'...", + Target::Url(package.file_name.clone()) + )); + + spinner.start(); + + package.extract().await?; + + spinner.finish(); + + Ok(()) +} diff --git a/pu/src/commands/run.rs b/pu/src/commands/run.rs new file mode 100644 index 0000000000000000000000000000000000000000..c19f713cd75e3d36743370de8d2eae1c3c276a74 --- /dev/null +++ b/pu/src/commands/run.rs @@ -0,0 +1,224 @@ +use anyhow::{anyhow, Result}; + +use libportutil::{build::PortBuild, get_pkg_root_dir, get_port_dir, get_port_name, get_work_dir}; + +use crate::{ + progress::SpinnerBar, + story::{MsgType, Story, Target}, +}; + +pub async fn run( + port_full_name: &str, + function: &str, + show_output: bool, + story: &mut Story, +) -> Result { + let name = get_port_name(port_full_name)?; + let mut return_code = 0; + + let mut port_builder = PortBuild::new( + get_work_dir(&name)?, + get_port_dir(port_full_name)?, + get_pkg_root_dir(&name)?, + num_cpus::get(), + show_output, + ); + + if function == "all" { + if show_output { + port_builder.build("all").await?; + } else { + story.begin(&format!( + "Start to build port '{}'", + Target::File(name.clone()) + )); + + match build_function_with_spinner(&mut port_builder, "prepare").await { + Ok(_) => story.line_with_type( + &format!( + "Successfully executed function '{}'", + Target::Function("prepare".to_string()) + ), + &MsgType::Ok, + ), + Err(err) => { + story.line_with_type( + &format!( + "Executed function '{}'\n{}", + Target::Function("prepare".to_string()), + err + ), + &MsgType::Error(true), + ); + } + } + + match build_function_with_spinner(&mut port_builder, "build").await { + Ok(_) => story.line_with_type( + &format!( + "Successfully executed function '{}'", + Target::Function("build".to_string()) + ), + &MsgType::Ok, + ), + Err(err) => { + story.line_with_type( + &format!( + "Executed function '{}'\n{}", + Target::Function("build".to_string()), + err + ), + &MsgType::Error(true), + ); + } + } + + match build_function_with_spinner(&mut port_builder, "preinst").await { + Ok(_) => story.line_with_type( + &format!( + "Successfully executed function '{}'", + Target::Function("preinst".to_string()) + ), + &MsgType::Ok, + ), + Err(err) => { + story.line_with_type( + &format!( + "Executed function '{}'\n{}", + Target::Function("preinst".to_string()), + err + ), + &MsgType::Error(true), + ); + } + } + + match build_function_with_spinner(&mut port_builder, "pkginst").await { + Ok(_) => story.line_with_type( + &format!( + "Successfully executed function '{}'", + Target::Function("pkginst".to_string()) + ), + &MsgType::Ok, + ), + Err(err) => { + story.line_with_type( + &format!( + "Executed function '{}'\n{}", + Target::Function("pkginst".to_string()), + err + ), + &MsgType::Error(true), + ); + } + } + + match build_function_with_spinner(&mut port_builder, "postinst").await { + Ok(_) => story.line_with_type( + &format!( + "Successfully executed function '{}'", + Target::Function("postinst".to_string()) + ), + &MsgType::Ok, + ), + Err(err) => { + story.line_with_type( + &format!( + "Executed function '{}'\n{}", + Target::Function("postinst".to_string()), + err + ), + &MsgType::Error(true), + ); + } + } + + story.end( + &format!( + "Successfully build port '{}' with time {} SBU", + Target::File(name), + Target::Url(port_builder.build_time().to_string()) + ), + &MsgType::Ok, + ); + } + } else { + if show_output { + return_code = port_builder.build(function).await?; + } else { + match build_function_with_spinner(&mut port_builder, function).await { + Ok(_) => story.line_with_type( + &format!( + "Successfully executed function '{}'", + Target::Function(function.to_string()) + ), + &MsgType::Ok, + ), + Err(err) => { + story.line_with_type( + &format!( + "Executed function '{}'\n{}", + Target::Function(function.to_string()), + err + ), + &MsgType::Error(true), + ); + } + } + } + + if return_code == 0 { + story.end( + &format!( + "Executed successfully '{}'", + Target::Function(function.to_string()) + ), + &MsgType::Ok, + ); + } else { + story.end( + &format!( + "Executed '{}' failure. Exit code: {}", + Target::Function(function.to_string()), + Target::Url(return_code.to_string()) + ), + &MsgType::Error(false), + ); + } + } + + Ok(port_builder.build_time()) +} + +async fn build_function_with_spinner(builder: &mut PortBuild, function: &str) -> Result<()> { + let mut spinner = SpinnerBar::new()?; + + spinner.set_message(format!( + "Running '{}'...", + Target::Function(function.to_string()) + )); + + spinner.set_spinner(vec![ + "[010010]".to_string(), + "[001100]".to_string(), + "[100101]".to_string(), + "[111010]".to_string(), + "[111101]".to_string(), + "[010111]".to_string(), + "[101011]".to_string(), + "[111000]".to_string(), + "[110011]".to_string(), + "[110101]".to_string(), + ]); + spinner.start(); + + let result = builder.build(function).await?; + + spinner.finish(); + + if result == 0 { + Ok(()) + } else { + Err(anyhow!(format!("Exit status: {}", result.to_string()))) + } +} diff --git a/pu/src/commands/update.rs b/pu/src/commands/update.rs new file mode 100644 index 0000000000000000000000000000000000000000..3a3428f74f8c1b3a3b66e0bf9245ccf04009d08e --- /dev/null +++ b/pu/src/commands/update.rs @@ -0,0 +1,267 @@ +use std::path::Path; + +use anyhow::{anyhow, Result}; +use colored::Colorize; +use tokio::fs; + +use libcport::{ + port::{PortConfig, PortConfigSection}, + traits::TomlConfig, +}; +use libportutil::{ + archive::Archive, clean_dir, execute, get_port_dir, get_port_name, get_work_dir, +}; + +use crate::{ + commands::{gen, run}, + story::{MsgType, Story, Target}, +}; + +use super::get; + +/// Update port +/// ## Arguments +/// * `container` use the `port-container` for build a port. (Not yet implemented) +/// * `port_full_name` a name of a port in format `category/name` +/// * `version` a port version to update to +/// * `url` an optional url for downloading an archive with the source code +pub async fn update( + container: bool, + port_full_name: &str, + version: &str, + url: &Option, + no_cleanup: bool, +) -> Result<()> { + let mut is_failed = false; + if container { + return Err(anyhow!("The container mode not yet implemented".red())); + } + + let name = get_port_name(port_full_name)?; + let port_dir = get_port_dir(port_full_name)?; + + let mut port_story = Story::new(0, "UPDATE"); + port_story.begin(&format!( + "Update port '{}'", + Target::File(port_full_name.to_string()) + )); + + // Prepare data for new port + let package = match url { + Some(url) => get::get(url, port_full_name, &mut Story::new(1, "PREPARE")).await?, + None => { + let work_dir = get_work_dir(&name)?; + let file = format!("{}/{}.ron", &work_dir, name); + let mut data_story = Story::new(1, "PREPARE"); + data_story.begin(&format!("Use prepared data '{}'", Target::Dir(work_dir))); + + data_story.line(&format!( + "Load package info from '{}'", + Target::File(file.clone()) + )); + + let ron = fs::read_to_string(&file).await?; + let pkg = Archive::from_ron(&ron)?; + data_story.end( + &format!("Package information loaded '{}'", Target::File(file)), + &MsgType::Ok, + ); + pkg + } + }; + + // Read port configuration + let toml_file = format!("{}/port.toml", &package.port_work_dir); + + port_story.line(&format!( + "Read port configuration from '{}'", + Target::File(toml_file.clone()) + )); + + let toml = fs::read_to_string(&toml_file).await?; + let mut port_config = PortConfig::from_str(&toml)?; + + // Update configuration + port_story.line(&format!("Update '{}'", Target::File(toml_file.clone()))); + + let old_version = port_config.package.version; + port_config.package.version = version.to_string(); + + // Update install file in the port directory + port_story.line(&format!( + "Update install file '{}'", + Target::File(format!("{}/install", &port_dir)) + )); + + // Update install file in the work directory + update_install( + &old_version, + &port_config.package.version, + &package.port_work_dir, + ) + .await?; + + let mut port_size = port_config.package.usage; + // Update files.list + // If file exists (manually created) just copy po the port directory + if Path::new(&format!("{}/files.list", package.port_work_dir)).exists() { + port_story.line(&format!( + "File '{}' already exists", + Target::Dir(format!("{}/files.list", package.port_work_dir)) + )); + } else { + let error_margin = f64::EPSILON; + // The new port is extracted only. Build it and generate files.list + let elapse = run::run(port_full_name, "all", false, &mut Story::new(1, "BUILD")).await?; + + if (elapse - port_config.package.build_time).abs() > error_margin || elapse > 0. { + port_config.package.build_time = if elapse < 0.1 { + 0.1 + } else { + f64::trunc(elapse * 100.0) / 100.0 + }; + } + + port_size = gen::files_list( + &package.port_root_dir, + Some(package.port_work_dir.clone()), + &mut Story::new(1, "FILES"), + ) + .await?; + + if port_size.abs() < error_margin { + is_failed = true; + } + } + + if is_failed { + port_story.end( + &format!( + "Failed to update '{}'", + Target::File(package.port_full_name.clone()) + ), + &MsgType::Error(false), + ); + } else { + // Update port.toml file + port_story.line(&format!( + "Update port metadata '{}'", + Target::File(toml_file.clone()) + )); + + let port_archive = PortConfigSection { + url: Some(package.url), + // Do not provide file name to avoid warnings in another tools. + // The archive name is part of url + file: None, + md5: Some(package.md5), + sha256: Some(package.sha256), + }; + port_config.package.usage = f64::trunc(port_size * 100.0) / 100.0; + port_config.port = Some(port_archive); + let toml = port_config.to_string()?; + fs::write(toml_file, toml).await?; + + // Update readme file + let readme_file = format!("{}/README.md", package.port_work_dir); + + port_story.line(&format!("Update '{}'", Target::File(readme_file.clone()))); + + fs::write(readme_file, port_config.get_markdown_string()).await?; + + port_story.line(&format!( + "All files prepared. Copy to the '{}'", + Target::Dir(port_dir.clone()) + )); + + // All files prepared. Copy to the port directory + copy_files(Path::new(&package.port_work_dir), Path::new(&port_dir))?; + + port_story.end( + &format!( + "Successfully updated '{}'", + Target::File(package.port_full_name.clone()) + ), + &MsgType::Ok, + ); + + if !no_cleanup { + let mut cleanup_story = Story::new(0, "CLEANUP"); + cleanup_story.begin("Cleanup"); + + cleanup_story.line(&format!( + "Clean up working directory '{}'", + Target::Dir(package.port_root_dir.clone()) + )); + + clean_dir(&package.port_root_dir).await?; + cleanup_story.end( + &format!( + "Successfully removed '{}'", + Target::Dir(package.port_root_dir.clone()) + ), + &MsgType::Ok, + ); + } + } + + Ok(()) +} + +/// Replace a port version to new one +async fn update_install(old_version: &str, new_version: &str, work_dir: &str) -> Result<()> { + if old_version == new_version { + // Nothing to do. + return Ok(()); + } + + let file = format!("{work_dir}/install"); + + let install = fs::read_to_string(&file).await?; + + let updated_install = install.replace( + &format!("VERSION=\"{old_version}\""), + &format!("VERSION=\"{new_version}\""), + ); + + // Return error if old_version not found in the install file + if updated_install == install { + return Err(anyhow!( + "Couldn't update {}. Version {} not found", + file.cyan(), + old_version.yellow() + )); + } + + fs::write(file, updated_install).await?; + + Ok(()) +} + +fn copy_files(source: &Path, destination: &Path) -> Result<()> { + execute::execute( + "cp", + vec![source.join("install"), destination.join("install")], + None, + )?; + + execute::execute( + "cp", + vec![source.join("port.toml"), destination.join("port.toml")], + None, + )?; + + execute::execute( + "cp", + vec![source.join("README.md"), destination.join("README.md")], + None, + )?; + + execute::execute( + "cp", + vec![source.join("files.list"), destination.join("files.list")], + None, + )?; + + Ok(()) +} diff --git a/pu/src/git.rs b/pu/src/git.rs new file mode 100644 index 0000000000000000000000000000000000000000..68375867f270dcc548aeea3c9e7fbfb9b7c902da --- /dev/null +++ b/pu/src/git.rs @@ -0,0 +1,53 @@ +use git2::Config; + +pub fn get_maintainer() -> Option { + let name = if let Some(users) = get_values("user.name") { + if users.is_empty() { + return None; + } + + users[0].clone() + } else { + return None; + }; + + let email = if let Some(emails) = get_values("user.email") { + if emails.is_empty() { + return None; + } + + emails[0].clone() + } else { + return None; + }; + + Some(format!("{} <{}>", name, email)) +} + +fn get_values(name: &str) -> Option> { + // TODO: Maybe we should use global instead? + let git_config = match Config::open_default() { + Ok(cfg) => cfg, + Err(_) => return None, + }; + + let entries = match git_config.entries(Some(name)) { + Ok(e) => e, + Err(_) => return None, + }; + + let mut values = Vec::new(); + entries + .for_each(|entry| { + let name = entry.name().unwrap_or_default().to_string(); + if !name.is_empty() { + let value = entry.value().unwrap_or_default().to_string(); + if !value.is_empty() { + values.push(value); + } + } + }) + .ok(); + + Some(values) +} diff --git a/pu/src/lib.rs b/pu/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..cb1f4df50a67dee898e5c61cfdc7de33ac702603 --- /dev/null +++ b/pu/src/lib.rs @@ -0,0 +1,5 @@ +pub mod cli; +pub mod commands; +pub mod git; +pub mod progress; +pub mod story; diff --git a/pu/src/progress.rs b/pu/src/progress.rs new file mode 100644 index 0000000000000000000000000000000000000000..9f13762bfb7e878100ce811a34055b31bac49cba --- /dev/null +++ b/pu/src/progress.rs @@ -0,0 +1,70 @@ +use std::time::Duration; + +use anyhow::Result; +use indicatif::{ProgressBar, ProgressStyle}; + +pub struct SpinnerBar { + pb: ProgressBar, + style: ProgressStyle, + tick: u64, + spinner: Vec, + message: String, +} + +impl SpinnerBar { + pub fn new() -> Result { + Ok(Self { + pb: ProgressBar::new_spinner(), + style: ProgressStyle::with_template("{spinner:.blue} {msg}")?, + tick: 120, + message: String::new(), + spinner: vec![ + "(● )".to_string(), + "( ● )".to_string(), + "( ● )".to_string(), + "( ● )".to_string(), + "( ● )".to_string(), + "( ●)".to_string(), + "( ● )".to_string(), + "( ● )".to_string(), + "( ● )".to_string(), + "( ● )".to_string(), + "(● )".to_string(), + ], + }) + } + + pub fn set_style(&mut self, style: ProgressStyle) { + self.style = style; + } + + pub fn set_tick(&mut self, tick: u64) { + self.tick = tick; + } + + pub fn set_spinner(&mut self, spinner: Vec) { + self.spinner = spinner; + } + + pub fn set_message(&mut self, msg: String) { + self.message = msg; + } + + pub fn finish_with_message(&mut self, msg: String) { + self.pb.finish_with_message(msg); + } + + pub fn finish(&self) { + self.pb.finish_and_clear(); + } + + pub fn start(&mut self) { + let spinner: Vec<_> = self.spinner.iter().map(String::as_str).collect(); + + self.pb.set_style(self.style.clone().tick_strings(&spinner)); + + self.pb.set_message(self.message.clone()); + + self.pb.enable_steady_tick(Duration::from_millis(self.tick)); + } +} diff --git a/pu/src/story.rs b/pu/src/story.rs new file mode 100644 index 0000000000000000000000000000000000000000..433f2f82d0a0283f1d4cfffc90317d22f778db7a --- /dev/null +++ b/pu/src/story.rs @@ -0,0 +1,111 @@ +use std::fmt::Display; + +use colored::Colorize; +use libmsg::{set_colors, Message}; + +const LEVEL_INDENTATION: usize = 3; +const LINE_INDENTATION: usize = 2; + +pub enum Target { + File(String), + Dir(String), + Url(String), + Function(String), +} + +#[derive(Clone)] +pub enum MsgType { + Ok, + Info, + Warning, + Error(bool), +} + +impl Display for Target { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Target::File(file) => write!(f, "{}", file.green()), + Target::Dir(dir) => write!(f, "{}", dir.blue()), + Target::Url(url) => write!(f, "{}", url.cyan()), + Target::Function(func) => write!(f, "{}", func.magenta()), + } + } +} + +pub struct Story { + name: String, + story_indentation: usize, + line_indentation: usize, + msg_provider: Message, +} + +impl Story { + #[must_use] + pub fn new(level: usize, name: &str) -> Self { + set_colors(true); + Self { + name: name.to_string(), + story_indentation: level * LEVEL_INDENTATION, + line_indentation: LINE_INDENTATION, + msg_provider: Message::default(), + } + } + + #[must_use] + pub fn get_story_indentation(&self) -> usize { + self.story_indentation + } + + #[must_use] + pub fn get_line_indentation(&self) -> usize { + self.line_indentation + } + + pub fn set_story_name(&mut self, name: &str) { + self.name = name.to_string(); + } + + pub fn set_story_level(&mut self, level: usize) { + self.story_indentation = level * LEVEL_INDENTATION; + } + + pub fn set_line_indentation(&mut self, indentation: usize) { + self.line_indentation = indentation; + } + + pub fn begin(&mut self, msg: &str) { + self.msg_provider.set_indent(self.story_indentation); + self.msg_provider.set_oper(&self.name); + self.msg_provider.info(msg); + } + + pub fn line(&mut self, msg: &str) { + self.msg_provider + .set_indent(self.story_indentation + self.line_indentation); + self.msg_provider.set_oper(&self.name); + self.msg_provider.msg(msg); + } + + pub fn line_with_type(&mut self, msg: &str, line_type: &MsgType) { + self.msg_provider + .set_indent(self.story_indentation + self.line_indentation); + self.msg_provider.set_oper(&self.name); + match line_type { + MsgType::Ok => self.msg_provider.ok(msg), + MsgType::Info => self.msg_provider.info(msg), + MsgType::Warning => self.msg_provider.warn(msg), + MsgType::Error(is_exit) => self.msg_provider.error(msg, *is_exit), + } + } + + pub fn end(&mut self, msg: &str, end: &MsgType) { + self.msg_provider.set_indent(self.story_indentation); + self.msg_provider.set_oper(&self.name); + match end { + MsgType::Ok => self.msg_provider.ok(msg), + MsgType::Info => self.msg_provider.info(msg), + MsgType::Warning => self.msg_provider.warn(msg), + MsgType::Error(is_exit) => self.msg_provider.error(msg, *is_exit), + } + } +} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 6ecd3b98da1848d6861c2010c72d4f58e4f24088..0000000000000000000000000000000000000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -toml~=0.10.2 -wget~=3.2 \ No newline at end of file diff --git a/setup.py b/setup.py deleted file mode 100644 index 3e6c1a0ba4869e902a3b982bb248d7251d706cc9..0000000000000000000000000000000000000000 --- a/setup.py +++ /dev/null @@ -1,13 +0,0 @@ -from distutils.core import setup - -setup( - name='cport', - version='v1.0b2', - packages=['libcport', 'cport_cli'], - package_dir={'libcport': 'src/libcport', 'cport_cli': 'src/cport_cli'}, - url='https://gitlab.com/calmiralinux/cabs/cport', - license='GNU GPLv3', - author='cov_id111', - author_email='michail383krasnov@mail.ru', - description='cport - менеджер системы портов дистрибутива Calmira GNU/Linux' -) diff --git a/src/cport.py b/src/cport.py deleted file mode 100755 index 0d7dc850e1d3376a75c1de7faabd8c00e99db4d8..0000000000000000000000000000000000000000 --- a/src/cport.py +++ /dev/null @@ -1,292 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import argparse -import os -import sys - -import cport_cli.args_parser as ccap # cport CLI Arguments Parser - CCAP -import libcport.core as cc -import libcport.datatypes as cd -from libcport.constants import DATABASE_FILE, DATABASE_USER_FILE, LOCAL_LIB_DIR, VERSION - -import cport_cli.info as cci -# import cport_cli.upgrade as ccU - -VER_MSG = f"""cport {VERSION} - the port manager for Calmira GNU/Linux-libre -Copyright (C) 2021, 2022 Michail Krasnov - -License GPLv3+: GNU GPL version 3 or later - -This is free software; you are free to change and redistribute it. -There is NO WARRANTY, to the extent permitted by law. -""" - -parser = argparse.ArgumentParser() -subparser = parser.add_subparsers() - -parser.add_argument( - "--install", "-i", nargs="+", help="Скачать, распаковать, собрать и установить порт в систему" -) - -parser.add_argument( - "--remove", "-r", nargs="+", help="Удалить порт из системы" -) - -parser.add_argument( - "--info", "-I", dest="info", nargs="+", - help="Просмотреть информацию о программном обеспечении в портах" -) - -parser.add_argument( - "--info-json", dest="info_json", nargs="+", - help="Просмотреть информацию о ПО в портах в формате JSON" -) - -parser.add_argument( - "--update", "-u", dest="update", action="store_true", - help="Проверить обновления системы портов" -) - -parser.add_argument( - "--upgrade", "-U", dest="upgrade", nargs="+", - help="Обновить указанное пользователем программное обеспечение" -) - -parser.add_argument( - "--get-flist", dest="get_flist", - help="Вывести список файлов указанного порта" -) - -list_parser = subparser.add_parser('list', help='list of ports') -list_parser.add_argument( - '-a', '--all', dest='list_all', action='store_true', - help="Просмотреть список всех портов" -) -list_parser.add_argument( - '-i', '--installed', dest='list_installed', action='store_true', - help="Просмотреть список установленных портов" -) -list_parser.add_argument( - '-I', '--not-installed', dest='list_not_installed', action='store_true', - help="Просмотреть список НЕ установленных портов" -) -list_parser.add_argument( - '-b', '--broken', dest='list_broken', action='store_true', - help="Просмотреть список битых портов" -) -list_parser.add_argument( - '-j', '--json', dest='list_json', action='store_true', - help="Вывести информацию о портах в формате JSON" -) -list_parser.set_defaults(func=ccap.ports_list) - -deps_parser = subparser.add_parser('deps', help='getting a list of packages that have the required one as a dependency') -deps_parser.add_argument( - "--all", "-a", dest="deps_all", help="all types of dependencies", - action="store_true" -) -deps_parser.add_argument( - "--required", "-r", dest="required_deps", help="required dependencies", - action="store_true" -) -deps_parser.add_argument( - "--recommend", "-R", dest="recommend_deps", help="recommend dependencies", - action="store_true" -) -deps_parser.add_argument( - "--optional", "-o", dest="optional_deps", help="optional dependencies", - action="store_true" -) -deps_parser.add_argument( - "--name", "-n", dest="dest_name", required=True, type=str, - help="set the name of port" -) -deps_parser.add_argument( - "--json", "-j", dest="deps_json", action="store_true" -) -deps_parser.set_defaults(func=ccap.port_deps) - -check_parser = subparser.add_parser('check', help='check all ports') -check_parser.add_argument( - '--is-exist', dest='check_exist', help="Проверка наличия порта в СП" -) -check_parser.add_argument( - '--is-installed', dest='check_installed', help="Проверка наличия порта в БД" -) -check_parser.add_argument( - '--check-ports', dest='check_all', action='store_true', - help="Проверка наличия зависимостей всех портов в СП" -) -check_parser.add_argument( - '--ports-linter', dest='plint', action='store_true', - help="Проверка портов на корректность" -) -check_parser.set_defaults(func=cci.ports_check) - -parser.add_argument( - "--yes", "-y", dest="confirm_proc", action="store_true", - help="Отвечать утвердительно на выполнение всех операций" -) - -parser.add_argument( - "--ignore-db", dest="ignore_db", type=bool, default=False, - help="Игнорировать проверку базы данных перед установкой или удалением порта" -) - -parser.add_argument( - "--skip-check", "-C", dest="skip_check", default=False, - action="store_true", help="Не проверять наличие или отсутствие файлов порта после установки или удаления" -) - -parser.add_argument( - "--rebuild-db", dest="rebuild_db", action="store_true", - help="Обновить базу данных cport" -) - -parser.add_argument( - "--wipe-cache", "-W", dest="wipe_cache", action="store_true", - help="Удалить все файлы, которые находятся в кеше" -) - -parser.add_argument( - "-v", "--version", action="store_true", - help="Print the cport version" -) - -args = parser.parse_args() - -if not args: - parser.print_usage() - exit(1) - -skip_checks = False -if args.skip_check: - skip_checks = True - -ignoredb = False -if args.ignore_db: - ignoredb = True - -confirm = False -if args.confirm_proc: - confirm = True - - -def main() -> int: - try: - if not os.path.exists(DATABASE_FILE): - cc.Msg.warn(f"File '{DATABASE_FILE}' not found! Rebuilding...") - if not cc.ckuser(): - cc.Msg.err("Permission error!") - return 126 - - cc.Msg.msg("-- cport init -- rebuild the database...") - db = cc.CDatabase(cd.port("general/vagina")) - - db.create_initial_db() - db.gen_all_ports() - - cc.Msg.msg("-- cport init -- regenerate the database...") - cc.rebuild() - - if not os.path.exists(LOCAL_LIB_DIR): - cc.Msg.warn(f"Directory '{LOCAL_LIB_DIR}' not found! Creating...") - os.makedirs(LOCAL_LIB_DIR) - - if not os.path.isfile(DATABASE_USER_FILE): - cc.Msg.warn(f"File '{DATABASE_USER_FILE}' not found! Rebuilding...") - - db = cc.CDatabase(cd.port("general/vagina"), dbfile=DATABASE_USER_FILE) - db.create_initial_db() - - if not vars(args): - parser.print_usage() - return 1 - - elif args.install: - if not cc.ckuser(): - cc.Msg.err("Permission denied!") - return 126 - return ccap.install(args.install, ignoredb, confirm, skip_checks) - - elif args.remove: - if not cc.ckuser(): - cc.Msg.err("Permission denied!") - return 126 - return ccap.remove(args.remove, ignoredb, confirm, skip_checks) - - elif args.info: - return ccap.info(args.info) - - elif args.info_json: - return ccap.info(args.info_json, json_enable=True) - - elif args.update: - if not cc.ckuser(): - cc.Msg.err("Permission denied!") - return 126 - return ccap.update(confirm) - - elif args.upgrade: - return ccap.upgrade(args.upgrade) - - elif args.get_flist: - return ccap.get_port_flist(args.get_flist) - - elif args.wipe_cache: - if not cc.ckuser(): - cc.Msg.err("Permission denied!") - return 126 - - return ccap.wipe_cache() - - elif args.rebuild_db: - if not cc.ckuser(): - cc.Msg.err("Permission denied!") - return 126 - - return ccap.rebuild_database() - - elif args.version: - print(VER_MSG) - return 0 - - else: - try: - code = args.func(args) - return code - except AttributeError: - parser.print_usage() - return 1 - - except KeyboardInterrupt: - cc.Msg.err("Keyboard interrupt.") - return 130 - - except EOFError: - cc.Msg.err("Keyboard interrupt.") - return 130 - - # except PermissionError: - # cc.Msg.err("Permission denied!") - # return 126 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/src/cport_cli/__init__.py b/src/cport_cli/__init__.py deleted file mode 100644 index d8d9549d301d9b3abeb14af8521919b278c30f84..0000000000000000000000000000000000000000 --- a/src/cport_cli/__init__.py +++ /dev/null @@ -1 +0,0 @@ -#!/usr/bin/python3 diff --git a/src/cport_cli/args_parser.py b/src/cport_cli/args_parser.py deleted file mode 100644 index cd51eba6e8053cbfec246c75e1246c9bf310eb23..0000000000000000000000000000000000000000 --- a/src/cport_cli/args_parser.py +++ /dev/null @@ -1,449 +0,0 @@ -#!/usr/bin/python3 -# -# args_parser.py -# -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -"""This module concatenates all other modules from `cport_shared`, creating -functions that will be used in `cport` when parsing command line arguments. The -module is a simple set of functions to abstract away the logic of the Calmira -GNU/Linux-libre distribution's port management utility. - -Examples: ---------- - ->>> import cport_cli.args_parser as ccap # (Cport Cli Argument Parser - ccap) ->>> import argparse ->>> parser = argparse.ArgumentParser() ->>> parser.add_argument( -... "--wipe-cache", "-W", action="store_true", dest="clean_cache", -... help="Clean the cache" -... ) -... ->>> args = parser.parse_args() ->>> if args.clean_cache: -... exit_value = ccap.wipe_cache() -... exit(exit_value) -... -""" -import json -import time - -import cport_cli.info -import cport_cli.install -import cport_cli.remove -import cport_cli.update -import cport_cli.upgrade - -import libcport.core as cc -import libcport.constants as ccc -import libcport.datatypes as cd -import libcport.exceptions as ce - -import toml - - -def wipe_cache() -> int: - r"""Function to delete all data in the cache - - Principle of operation - ---------------------- - - 1. A list of all files in the cache is calculated (which is only needed - to output to the console how many files have been deleted). - 2. If there are 0 files, then an error message is displayed: there is - nothing to delete. - - Returns 1 - 3. If there are more than zero files, then all of them are deleted. A - message is printed to `stdout` stating that they have all been removed. - - Returns 0 - """ - - files = cc.get_files_in_cache() - cc.wipe_cache() - if files > 0: - cc.Msg.ok(f"{files} files have been deleted") - return 0 - else: - cc.Msg.err("Nothing to delete") - return 1 - - -def port_flist(prt: str) -> int: - """Function to list port files to stdout - - Principle of operation - ---------------------- - - 1. The `files.list` file of the port is opened and read. - 2. Its content is output to stdout. - - Returns 0. - 3. If the `files.list` file or the port itself is missing, a message about - this error is displayed in stderr. - - Returns 1. - """ - - try: - port = cd.port(prt) - fl = f"{port.path}/files.list" - with open(fl) as f: - print(f.read()) - return 0 - except ce.PortNotFoundError: - cc.Msg.err(f"Port '{prt}' not found!") - return 1 - except FileNotFoundError: - cc.Msg.err(f"File 'files.list' of port '{prt} not found") - return 1 - - -def install(ports: list[str], ign_db: bool = False, cnfrm: bool = True, skip_chk: bool = False) -> int: - try: - _info = cport_cli.info.CPortDependenciesCLI(list(map(cd.port, ports))) - except ce.PortNotFoundError: - cc.Msg.err(f"Some of ports not found!") - return 1 - - _info.print_selected_ports() - _info.print_deps() - _info.print_usage() - - if not cnfrm and not cc.Msg.dialog(): - return 1 - - for port in ports: - try: - port = cd.port(port) - - cc.Msg.msg(f"Installing port '{port.name}'") - - # run = cport_cli.install(ignore_db=ign_db, confirm=cnfrm, skip_check=skip_chk) - run = cport_cli.install.install(port, ign_db, skip_chk) - - if run != 0: - return run - except ce.PortNotFoundError: - cc.Msg.err(f"Port '{port}' not found!") - return 1 - except toml.decoder.TomlDecodeError: - cc.Msg.err(f"Error decoding the configuration file of the port '{port}'") - return 1 - # except ValueError: - # cc.Msg.err(f"File '{port.path}/port.toml' is broken!") - # return 1 - return 0 - - -def remove(ports: list[str], ignore_db: bool = False, cnfrm: bool = False, skip_check: bool = False) -> int: - for port in ports: - try: - port = cd.port(port) - - cc.Msg.header(f"Removing port '{port.name}'") - run = cport_cli.remove.remove(port, ignore_db, cnfrm, skip_check) - - if run != 0: - return run - except ce.PortNotFoundError: - cc.Msg.err(f"Port '{port}' not found!") - return 1 - except toml.decoder.TomlDecodeError: - cc.Msg.err(f"Error decoding the configuration file of the port '{port}'") - return 1 - return 0 - - -def info(ports: list[str], json_enable: bool = False) -> int: - try: - ports = list(map(cd.port, ports)) - - if json_enable: - inf = cport_cli.info.CPortInfoJSON(ports) - else: - inf = cport_cli.info.CPortInfoCLI(ports) - - inf.info() - - return 0 - except ce.PortNotFoundError: - cc.Msg.err("Unknown port not found!") - return 1 - - -def is_installed(port: str) -> int: - return cport_cli.info.is_installed(port) - - -def is_exist(port: str) -> int: - return cport_cli.info.is_exist(port) - - -def ports_list(args) -> int: - cl = cc.CList() - # dplst = ('required', 'recommend', 'optional') - - pkg_list = cl.all() - - pkg_list_i = cl.installed() - pkg_list_n = cl.not_installed() - pkg_list_b = cl.broken() - - pkg_list_it = [] - pkg_list_nt = [] - pkg_list_bt = [] - - for pkg in pkg_list_i: - pkg_list_it.append(pkg[0]) - for pkg in pkg_list_n: - pkg_list_nt.append(pkg) - for pkg in pkg_list_b: - pkg_list_bt.append(pkg) - - if args.list_json: - json_enable = True - else: - json_enable = False - - def get_json(mode: str = "all") -> str: - # dt = {} - if mode == "all": - dt = { - "installed": [], - "not_installed": [], - "broken": [], - "unknown_status": [] - } - - for _package in pkg_list: - if _package in pkg_list_it: - dt['installed'].append(_package) - elif _package in pkg_list_n: - dt['not_installed'].append(_package) - elif _package in pkg_list_b: - dt['broken'].append(_package) - else: - dt['unknown_status'].append(_package) - - elif mode == "installed": - dt = { - "installed": [] - } - for _package in pkg_list_i: - _dt = { - "name": _package[0], - "installation_date": _package[1] - } - dt['installed'].append(_dt) - - elif mode == "not_installed": - dt = { - "not_installed": [] - } - for _package in pkg_list_n: - dt['not_installed'].append(_package) - - elif mode == "broken": - dt = { - "broken": [] - } - for _package in pkg_list_b: - dt['broken'].append(_package) - - else: - raise ValueError - - return json.dumps(dt, indent=4, ensure_ascii=False) - - if args.list_all: - if json_enable: - print(get_json("all")) - return 0 - - print("\033[3m\033[42m \033[0m - installed ports;", end=" ") - print("\033[3m\033[47m \033[0m - not installed ports;", end=" ") - print(f"\033[3m\033[41m \033[0m - broken ports\n{'-' * 80}") - - for package in pkg_list: - if package in pkg_list_it: - print(f"\033[3m\033[42m \033[0m {package}") - elif package in pkg_list_nt: - print(f"\033[3m\033[47m \033[0m {package}") - elif package in pkg_list_bt: - print(f"\033[3m\033[41m \033[0m {package} - \033[31mbroken\033[0m") - - return 0 - - elif args.list_installed: - if json_enable: - print(get_json("installed")) - return 0 - pkg_lst_all = cl.installed() - - print("\033[1m{0:30} {1}\n{2}\033[0m".format( - "port name", "installation date", 55 * '-' - )) - - for pkg in pkg_lst_all: - tp_str = "{0:30} {1}".format(pkg[0], time.ctime(float(pkg[1]))) - print(tp_str) - - return 0 - - elif args.list_not_installed: - if json_enable: - print(get_json("not_installed")) - return 0 - pkg_list = pkg_list_n - elif args.list_broken: - if json_enable: - print(get_json("broken")) - return 0 - pkg_list = pkg_list_b - - else: - if json_enable: - print(get_json()) - return 0 - - for package in pkg_list: - print(package) - - return 0 - - -def update(confirm: bool = False) -> int: - dt = cport_cli.update.check() - if dt == 1: - return 1 - elif dt == -1: - return 0 - - if not confirm and not cc.Msg.dialog(): - return 1 - - cc.Msg.msg("Updating the ports system...") - u = cport_cli.update.Update(f"{ccc.CACHE_UNPACKED_DIR}/ports") - if u.install(): - cc.Msg.ok("Update complete!") - cc.Msg.msg("Update the database...") - - db = cc.CDatabase(cd.port("general/vagina")) - db.gen_all_ports() - db.close() - - return 0 - else: - cc.Msg.err("Update FAIL!") - return 1 - - -def upgrade(args) -> int: - return cport_cli.upgrade.main(args) - - -def rebuild_database() -> int: - cc.rebuild() - return 0 - - -""" - elif args.list_deps: - if json_enable: - print(get_json("deps")) - return 0 - dps = [] - for dep in dplst: - for pkg in pkg_list_d[dep]: - dps.append(f"\033[1m{dep}:\033[0m\t{pkg}") - pkg_list = dps -""" - - -def port_deps(args) -> int: - def get_json(_mode: str = "all") -> str: - """Work modes: - - * all [default]; - * required; - * recommend; - * optional - """ - - dt = { - "port_name": prt.name - } - - if _mode == "all": - for _dep in dplst: - dt[_dep] = pkg_lst.get(_dep) - else: - dt[_mode] = pkg_lst.get(_mode) - - return json.dumps(dt, indent=4, ensure_ascii=False) - - dplst = ccc.PORT_TOML_PARAMS['deps'] - - if args.deps_json: - json_enable = True - else: - json_enable = False - - prt_name = args.dest_name - - try: - prt = cd.port(prt_name) - except ce.PortNotFoundError: - cc.Msg.err(f"Port '{prt_name}' not found!") - return 1 - - pkg_lst = cport_cli.info.get_deps(prt) - - if args.required_deps: - mode = 'required' - elif args.recommend_deps: - mode = 'recommend' - elif args.optional_deps: - mode = 'optional' - else: - mode = 'all' - - if json_enable: - print(get_json(mode)) - return 0 - - if mode == "all": - for mode in dplst: - for dep in pkg_lst[mode]: - print(f"\033[1m{mode}:\t\033[0m {dep}") - return 0 - - for dep in pkg_lst[mode]: - print(dep) - - -def get_port_flist(port: str) -> int: - try: - port = cd.port(port) - with open(f"{port.path}/files.list") as f: - print(f.read()) - return 0 - except FileNotFoundError: - cc.Msg.err(f"File '{port.path}/files.list' not found!") - return 1 - except ce.PortNotFoundError: - cc.Msg.err(f"Port '{port}' not found!") - return 1 diff --git a/src/cport_cli/bs.py b/src/cport_cli/bs.py deleted file mode 100644 index 3b1f41d58e66c20fde0eae6458680715405614a4..0000000000000000000000000000000000000000 --- a/src/cport_cli/bs.py +++ /dev/null @@ -1,282 +0,0 @@ -#!/usr/bin/python3 -# -# bs.py - build system for Calmira GNU/Linux-libre -# -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -raise DeprecationWarning - -import os -import shutil -import subprocess -import tarfile -import time - -import libcport.constants as cC -import libcport.datatypes as cd -import toml -import wget -from cport_cli.shared import Msg - -CACHE_DIR = "/mnt/calm/sources" -INFO_FILE = "/mnt/calm/build.toml" -PREPARATION_SCRIPTS_DIR = f"{cC.PORTS_DIR}/bs/scripts" -CREATE_DIRS = f"{PREPARATION_SCRIPTS_DIR}/dirs.sh" -CREATE_USER = f"{PREPARATION_SCRIPTS_DIR}/users.sh" - -""" -Automated Building System for Calmira GNU/Linux-libre - -Запускается от имени пользователя `calm'. Собирает порты для временного иструментания, -после входит в chroot и собирает порты core-редакции. -""" - - -class BuildingPreparations(object): - """Carrying out preliminary preparations: - - - Creation of necessary files and directories for system building; - - Checking the correctness of the host system for the building; - """ - - def __init__(self): - if os.geteuid() != 0: - raise PermissionError - - def _run(self, script: str) -> int: - run = subprocess.run(script, shell=True) - return run.returncode - - def create_limited_dirs_lyout(self) -> int: - return self._run(CREATE_DIRS) - - def create_user(self) -> int: - return self._run(CREATE_USER) - - -class BuildMachine(object): - """Класс для сборки конкретного порта. - - Все необходимые порты [в нужном порядке] для сборки перечислены в файле - '/usr/ports/bs/build_order.toml'. Там же содержатся сведения о системе, - которую требуется собрать: её название, версия, номер билда и кодовое - имя релиза. - """ - - def __init__(self, port: cd.port): - self.port = port - self.metadata = toml.load(f"{port.path}/port.toml") - - def _get_fname(self): - url = self.metadata['port'].get('url') - file = self.metadata['port'].get('file') - - if url is None: - raise ValueError - - if file is not None: - return file - - return wget.detect_filename(url) - - def _get_port_info(self) -> None: - return - - def download(self): - if self.metadata['port']['url'] == "none": - return True - - file = f"{CACHE_DIR}/{self._get_fname()}" - - wget.download(self.metadata['port']['url'], CACHE_DIR) - print() - - return os.path.isfile(file) - - def unpack(self) -> bool: - if self.metadata['port']['url'] == "none": - return True - - if self.metadata['port'].get('file') is not None: - file = self.metadata['port']['file'] - else: - file = f"{CACHE_DIR}/{self._get_fname()}" - - try: - with tarfile.open(file, 'r') as f: - f.extractall(path=CACHE_DIR) - except tarfile.ReadError: - Msg.err(f"Error reading file '{file}'!") - return False - except tarfile.CompressionError: - Msg.err(f"File '{file}' compression error!") - return False - except tarfile.ExtractError: - Msg.err(f"Error unpacking file '{file}'!") - return False - - return True - - def install(self) -> int: - script = f"{self.port.path}/install" - run = subprocess.run(script, shell=True) - - return run.returncode - - -class BuildProgress(object): - """A class for getting information about ports that have already been collected. - - Arguments: - - :arg port: str # Optional argument. If not specified, the - # 'check_builded' and 'set_builded' methods will not - # work. - - Methods: - - - check_builded() - checks if the specified port is built or not - - set_builded() - sets the specified port as built - - set_build_time() - sets the time of building all ports - - get_builded_ports() - Gets a list of all built ports - """ - - def __init__(self, port=None): - self.port = port - self.data = toml.load(INFO_FILE) - - if not os.path.isfile(INFO_FILE): - Msg.err(f"File '{INFO_FILE}' is not a file or not found") - - if os.path.isdir(INFO_FILE): shutil.rmtree(INFO_FILE) - - if not os.path.exists(INFO_FILE): - Msg.err(f"File '{INFO_FILE}' not found in the filesystem!") - Msg.msg("Create a base configuration in the '{INFO_FILE}'...") - - self._create_initial_conf() - - def _create_initial_conf(self) -> None: - """Creates a base configuration file ('cport_cli.bs.INFO_FILE') if - assigned.""" - - data = { - "ports": { - "builded": [], - "build_time": 0 - } - } - - with open(INFO_FILE, 'w') as f: - toml.dump(data, f) - - def _set_config(self) -> None: - """This method writes the 'self.data' value to the INFO_FILE config file - after doing something to change the config""" - - with open(INFO_FILE, 'w') as f: - toml.dump(self.data, f) - - def check_builded(self) -> bool: - if self.port is not None: - return self.port in self.data['ports']['builded'] - else: - raise ValueError - - def set_builded(self) -> None: - self.data['ports']['builded'].append(self.port) - self._set_config() - - # with open(INFO_FILE, 'w') as f: - # toml.dump(self.data, f) - - def set_build_time(self, tm: float) -> None: - _tm = self.data['ports']['build_time'] - self.data['ports']['build_time'] = _tm + tm - self._set_config() - - # with open(INFO_FILE, 'w') as f: - # toml.dump(self.data, f) - - def get_builded_ports(self) -> list[str]: - return self.data['ports']['builded'] - - -def build_pkgs(edition: str = "toolchain") -> int: - md = toml.load(f"{cC.PORTS_DIR}/bs/build_order.toml") - ports = md['editions'][edition] - - count = 1 - count_all = len(ports) - - Msg.msg("Start the build machine...") - time_start_bs = time.time() - - for port in ports: - progress = BuildProgress(port) - if progress.check_builded(): - Msg.msg(f"Port '{port}' is already builded") - continue - - Msg.msg(f"({count}/{count_all}) Install port '{port}'...") - - bs = BuildMachine(cd.port(port)) - time_start = time.time() - - Msg.msg(" Download port...") - r = bs.download() - - if not r: - return 1 - - Msg.msg(" Unpack port...") - r = bs.unpack() - - if not r: - return 1 - - Msg.msg(" Build port...") - r = bs.install() - - time_end = time.time() - Msg.msg(f"Build time (seconds): {time_end - time_start}") - - if r != 0: - Msg.err(f"Port '{port}' returned a value other then 0! Rebuild it (1), break (2) or continue building (3)?") - run = input() - Msg.log_msg(f" input: '{run}'") - - if run == "1": - build_pkgs(edition) - elif run == "3": - progress.set_builded() - continue - elif run == "2": - return r - else: - Msg.err("Unknown input!") - return r - - count += 1 - progress.set_builded() - - time_end_bs = time.time() - - progress_t = BuildProgress() - progress_t.set_build_time(time_end_bs - time_start_bs) - - return 0 diff --git a/src/cport_cli/info.py b/src/cport_cli/info.py deleted file mode 100644 index 5a8c0b5f003e0f22b793cc3e2c613359a14c5450..0000000000000000000000000000000000000000 --- a/src/cport_cli/info.py +++ /dev/null @@ -1,540 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import json -import toml - -import libcport.constants as ccc -import libcport.exceptions as ce -import libcport.core as cc -import libcport.datatypes as cd -import libcport.info as ci -import libcport.dependencies as ccd - - -class CPortDependenciesCLI: - - def __init__(self, ports: list[cd.port]): - self.ports: list[cd.port] = ports - self.port_deps: list[cd.port] = [] - - def _get_selected_ports(self) -> list[dict[str, str, bool]]: - """Returns information about user-selected ports: - - [ - { - "name": "base/acl", - "version": "1.1", - "installed": True - }, - { - "name": "base/attr", - "version": "1.2", - "installed": False - } - ... - ] - """ - - ports: list[dict] = [] - - for port in self.ports: - try: - _info = ci.CPortInfo(port) - pkg: dict = _info.package() - - data = { - "name": pkg['name'], - "version": pkg['version'], - "installed": pkg['installed'] - } - - if port not in ports: - ports.append(data) - except ce.PortNotFoundError as e: - cc.Msg.err(f"Port '{e}' not found!") - break - - return ports - - def _get_dependencies(self) -> list[dict[str, str]]: - """Returns a list of all required software for installation: - - [ - {'name': 'base/m4', 'version': '1.4.19'}, - {'name': 'base/shadow', 'version': '4.12.3'}, - {'name': 'base/openssl', 'version': '3.0.5'}, - {'name': 'base/perl', 'version': '5.36.0'}, - {'name': 'base/readline', 'version': '8.1.2'}, - {'name': 'base/ncurses', 'version': '6.3'} - ]""" - - ports: list[dict] = [] - ports_processed: list[str] = [] - - for port in self.ports: - try: - cdr = ccd.CDependencyResolver(port) - deplst = cdr.gen_deplist() - - for dep in deplst: - _info = ci.CPortInfo(cd.port(dep)) - pkg = _info.package() - - data = { - "name": dep, - "version": pkg['version'] - } - - condition = all([ - data not in self.port_deps, - cd.port(data['name']) not in self.ports, - data['name'] not in ports_processed - ]) - - if condition: - self.port_deps.append(cd.port(dep)) - ports.append(data) - ports_processed.append(data['name']) - - except ce.PortNotFoundError as e: - cc.Msg.err(f"Port '{e}' not found!") - break - - return ports - - def _calculate_usage(self) -> float: - usage = 0.0 - for category in self.ports, self.port_deps: - for port in category: - _info = ci.CPortInfo(port) - pkg = _info.package() - - usage += float(pkg['usage']) - - return usage - - def print_selected_ports(self) -> None: - ports = self._get_selected_ports() - - print("\033[1mSelected ports:\033[0m") - print("\t", end=" ") - - for port in ports: - print(f"{port['name']}-{port['version']}", end=" ") - print("\n") - - def print_deps(self) -> None: - ports = self._get_dependencies() - - if len(ports) == 0: - return - - print("\033[1mPorts that are not installed on the system:\033[0m") - print("\t", end=" ") - - for port in ports: - print(f"{port['name']}-{port['version']}", end=" ") - print("\n") - - def print_usage(self) -> None: - print("\033[1mHow much space will be taken up on the disk:\033[0m", end=" ") - print(f"{self._calculate_usage()} Mb") - - -class CPortDependenciesJSON(CPortDependenciesCLI): - - def __init__(self, ports: list[cd.port]): - super().__init__(ports) - - def print_selected_ports(self) -> None: - print("\033[1mSelected ports:\033[0m") - data: str = json.dumps(self._get_selected_ports(), indent=4) - print(data) - - def print_deps(self) -> None: - print("\033[1mPorts that are not installed on the system:\033[0m") - data: str = json.dumps(self._get_dependencies(), indent=4) - print(data) - - -class CPortInfoCLI: - - def __init__(self, ports: list[cd.port]): - self.ports: list[cd.port] = ports - - def _data(self, port: cd.port, mode: str): - info = ci.CPortInfo(port) - modes = { - "package": { - "func": info.package, - "msg": "Base information:" - }, - "deps": { - "func": info.deps, - "msg": "\nDependencies:" - } - } - return modes[mode]['func'], modes - - def _print(self, port: cd.port, mode: str): - if port not in self.ports: - return - - tp_str = "" - func, modes = self._data(port, mode) - - if func() is None or len(func().keys()) == 0: - return - - print(f"\033[1m{modes[mode]['msg']}") - - for i in func().keys(): - if type(func()[i]) == list: - for index in func()[i]: - tp_str += str(index) + " " - else: - tp_str = func()[i] - - print("\033[1m{0:12}:\033[0m {1}".format(i, tp_str)) - tp_str = "" # Clear string... - - def info(self): - for port in self.ports: - cc.Msg.header(f"Information about port '{port.name}'") - - for mode in "package", "deps": - self._print(port, mode) - - print("\n") - - -class CPortInfoJSON(CPortInfoCLI): - - def __init__(self, ports: list[cd.port]): - super().__init__(ports) - - def info(self): - lst = [] - for port in self.ports: - dt = {'name': port.name} - - for mode in "package", "deps": - md, _ = self._data(port, mode) - dt[mode] = md() - lst.append(dt) - - print(json.dumps(lst, indent=4)) - # print(lst) - - -def is_installed(name: str) -> int: - try: - name = cd.port(name) - db = cc.CDatabase(name) - except ce.PortNotFoundError: - cc.Msg.err(f"Port '{name}' not found!") - return 1 - except ce.PortBrokenError: - cc.Msg.err(f"Port '{name}' is broken!") - return 1 - except toml.decoder.TomlDecodeError: - cc.Msg.err(f"Error decoding the configuration file of the port '{name}'!") - return 1 - - print("{:15} :".format(name.name), end=" ") - if db.check(): - print("installed") - code = 0 - else: - print("not installed") - code = 1 - - db.close() - return code - - -def is_exist(port: str) -> bool: - try: - cd.port(port) - print("{0:15}: {1}".format(port, "exist")) - return True - except ce.PortNotFoundError: - print("{0:15}: {1}".format(port, "not exist")) - return False - except ce.PortBrokenError: - cc.Msg.err(f"Port '{port}' is broken!") - return False - except toml.decoder.TomlDecodeError: - cc.Msg.err(f"Error decoding the configuration file of the port '{port}'!") - return False - - -def ports_check(args) -> int: - """ - elif args.check_all: - return check_deps() - elif args.plint: - return port_linter() - """ - - if args.check_exist: - if is_exist(args.check_exist): - return 0 - else: - return 1 - elif args.check_installed: - return is_installed(args.check_installed) - else: - data = toml.load(ccc.METADATA_FILE) - - releases = "" - categories = "" - - for i in data['system']['release']: - releases += str(i) + " " - - for i in data['port_sys']['categories']: - categories += str(i) + " " - - print("\033[1mBase information about ports system:\033[0m") - print("{0:16}: {1}".format('calmira releases', releases)) - print("{0:16}: {1}".format('categories', categories)) - - return 0 - - -""" -def json_info(name: port) -> None: - conf = toml.load(f"{name.path}/port.toml") - print(json.dumps(conf, ensure_ascii=False, indent=4)) - - -def info(name: port) -> None: - conf = toml.load(f"{name.path}/port.toml") - package = [ - "name", "version", "maintainer", - "releases", "priority", "usage", - "upgrade_mode", "build_time", - "description" - ] - deps = [ - "required", "recommend", - "optional", "conflict" - ] - - package_conf = conf.get('package') - deps_conf = conf.get('deps') - - def form(params: list, data, header: str, prt: bool = False) -> None: - tp_str = "" - if data is None: - return - - print(f"\033[1m{header}:\033[0m") - for param in params: - if data.get(param) is not None: - print("{:15}:".format(param), end=" ") - if type(data[param]) == list: - for i in data[param]: - if prt: - # TODO: добавить выделение портов жёлтым цветом в - # случае, если порт установлен в систему, однако в - # системе отсутствуют какие-либо файлы (статусы 2 и - # 3), либо версия в базе данных не соответствует - # версии в системе портов. - try: - # В том случае, если функция вызывается для - # показа зависимостей, то мы берём каждую из - # зависимостей и проверяем её наличие в базе - # данных. В том случае, если порт в базе - # присутствует, то выделить его зелёным цветом. - # Если порт в БД отсутствует, то не выделять - # никак. Если порт отсутствует в системе портов - # вообще, то выделить его красным цветом. - port_i = port(i) - db = CDatabase(port_i) - if db.check(): - i = f"\033[32m{i}\033[0m" - db.close() - except ce.PortNotFoundError: - i = f"\033[31m{i}\033[0m" - tp_str += str(i) + " " - print(tp_str) - tp_str = "" - else: - print(data[param]) - - cd = CDatabase(name) - form(package, package_conf, "Base information") - print("{0:15}: {1}".format("installed", cd.check())) - form(deps, deps_conf, "\nDependencies", prt=True) - cd.close() -""" - - -def port_linter() -> int: - cl = cc.CList() - code = 0 - - ports = cl.all() - - for prt in ports: - try: - data = cc.p_lint(prt) - - if data: - cc.Msg.ok(f"Port '{prt}': ok") - else: - cc.Msg.err(f"Port '{prt}': unknown error!") - code = 1 - except ce.FilePortTomlError: - cc.Msg.err(f"Port '{prt}': 'port.toml' file is broken or doesn't exist!") - except ce.FileFilesListError: - cc.Msg.err(f"Port '{prt}': 'files.list' file is broken or doesn't exist!") - - return code - - -def check_deps() -> int: - cl = cc.CList() - ports = cl.all() - ports_broken = cl.broken() - - deps_types = ('required', 'recommend', 'optional', 'conflict') - - used_prts = [] # Уже проверенные порты - errors = [] # Отсутствующие порты - broken = 0 # Битые порты - oks = [] # Присутствующие порты - - print("\033[3m\033[41m \033[0m - depends exists;", end=" ") - print(f"\033[3m\033[42m \033[0m - depends does not exist\n{'-' * 80}") - - for name in ports: - if name in ports_broken: - cc.Msg.err(f"Port '{name}' is broken!") - broken += 1 - continue - - name = cd.port(name) - - prt_data = toml.load(f"{name.path}/port.toml") - prt_deps = prt_data['deps'] - - for dep_type in deps_types: - if prt_deps.get(dep_type) is None: - # Пропуск в том случае, если у порта нет зависимостей - continue - deps = prt_deps[dep_type] - - for dep in deps: - if dep in used_prts: - # Пропуск в том случае, если порт уже проверен - проверенные - # порты добавляются в список 'used_prts' - continue - - if dep in ports: - oks.append(dep) - used_prts.append(dep) - else: - errors.append(dep) - used_prts.append(dep) - - if len(errors) > 0 or len(oks) > 0: - used_prts.sort() - for name in used_prts: - if name in errors: - print(f"\033[3m\033[41m \033[0m {name}") - if name in oks: - print(f"\033[3m\033[42m \033[0m {name}") - - print(f"Existing depends: \033[32m{len(oks)}\033[0m") - print(f"Non-existing depends: \033[31m{len(errors)}\033[0m") - print(f"Broken depends: \033[31m{broken}\033[0m") - return 0 - else: - return 0 - - -def get_deps(name: cd.port) -> dict: - """ - Returns a dictionary listing all ports that have the specified port as a - dependency. - - Return value: - - { - "required": [], - "recommend": [], - "optional": [] - } - """ - - cl = cc.CList() - all_ports = cl.all() - suggest = { - "required": [], - "recommend": [], - "optional": [] - } - dplst = ('required', 'recommend', 'optional') - - for prt in all_ports: - prt_dt = cd.port(prt) - data = toml.load(f"{prt_dt.path}/port.toml") - deps = {} - - for dep in dplst: - deps[dep] = data['deps'].get(dep) - - for dep in dplst: - if deps[dep] is not None: - if name.name in deps[dep]: - suggest[dep].append(prt) - return suggest - - -def ports_check(args) -> int: - # FIXME: функция не работает!!! - if args.check_exist: - return is_exist(args.check_exist) - elif args.check_installed: - return is_installed(args.check_installed) - elif args.check_all: - return check_deps() - elif args.plint: - return port_linter() - else: - cl = cc.CList() - data = cl.all() - - releases = "" - categories = "" - - for i in data['system']['release']: - releases += str(i) + " " - - for i in data['port_sys']['categories']: - categories += str(i) + " " - - print("\033[1mBase information about ports system:\033[0m") - print("{0:16}: {1}".format('calmira releases', releases)) - print("{0:16}: {1}".format('categories', categories)) - - return 0 diff --git a/src/cport_cli/install.py b/src/cport_cli/install.py deleted file mode 100644 index c9791432299688b5c6aadfaf87c6a779b659bca3..0000000000000000000000000000000000000000 --- a/src/cport_cli/install.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import tarfile -import zipfile -from urllib.error import URLError - -import libcport.core as cc -import libcport.datatypes as cd -import libcport.install as ci - - -def install(port: cd.port, ignore_db: bool, skip_check: bool) -> int: - db = cc.CDatabase(port) - inst = ci.CInstall(port) - - if db.check() and not ignore_db: - cc.Msg.warn(f"Port '{port.name}' already in the database!") - cc.Msg.msg("Rebuilding...") - - try: - if inst.check_fname(): - cc.Msg.msg(f"Download port '{port.name}'...") - if inst.download(): - cc.Msg.ok("Download OK") - else: - cc.Msg.err("Download \033[1mERROR\033[0m") - return 1 - - cc.Msg.msg("Validating source file with md5sums...", endl=" ") - if inst.cksum(): - print("Passed") - else: - cc.Msg.warn("NOT passed") - run = cc.Msg.dialog() - - if not run: - cc.Msg.err("Aborted!") - return 1 - - cc.Msg.msg(f"Extracting '{port.name}'...") - inst.unpack() - - cc.Msg.msg(f"Building port '{port.name}'...") - - run = inst.install() - - if run != 0: - cc.Msg.err(f"Build instructions was returned a value other then 0!") - return run - - cc.Msg.ok("Build OK!") - - install_status = 0 - - if not ignore_db: - try: - install_status = cc.check(port) - cc.Msg.msg("Updating the database...", endl=" ") - db.add(status=install_status) - print("OK") - except FileNotFoundError: - cc.Msg.err(f"File '{port.path}/files.list' not found!") - return 1 - - if not skip_check: - cc.Msg.msg(f"Checking the port '{port.name}' for correct installation...", endl=" ") - if install_status == 0: - cc.Msg.warn("Some port files were not found!") - elif install_status == 3: - cc.Msg.err("Port installation error!") - run = 1 - else: - cc.Msg.ok("Checking completed successfully") - - cc.Msg.msg("Clearing the cache...") - fcount = cc.get_files_in_cache() - cc.wipe_cache() - cc.Msg.ok(f"{fcount} files was deleted!") - - return run - except zipfile.BadZipFile: - cc.Msg.err(f"Bad ZIP file '{inst.get_fname()}'!") - return 1 - except tarfile.ReadError: - cc.Msg.err(f"Error reading file '{inst.get_fname()}'!") - return 1 - except tarfile.CompressionError: - cc.Msg.err(f"File '{inst.get_fname()}' compression error!") - return 1 - except tarfile.ExtractError: - cc.Msg.err(f"Error unpacking file '{inst.get_fname()}'!") - return 1 - except URLError: - cc.Msg.err("Temporary failure in name resolution!") - return 1 diff --git a/src/cport_cli/remove.py b/src/cport_cli/remove.py deleted file mode 100644 index fc1d1ab9b2a8c8e836c021272a50aa74432ac887..0000000000000000000000000000000000000000 --- a/src/cport_cli/remove.py +++ /dev/null @@ -1,93 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import libcport.core as cc -import libcport.exceptions as ce -import libcport.remove as cr -import libcport.datatypes as cd - - -def remove(port: cd.port, ignore_db: bool = False, confirm: bool = False, skip_check: bool = False) -> int: - db = cc.CDatabase(port) - - if not db.check() and not ignore_db: - cc.Msg.err(f"Port '{port.name}' doesn't exist in the database!") - return 1 - - rm = cr.CRemove(port) - - if rm.check_priority(): - cc.Msg.err(f"Port '{port.name}' has system priority") - return 1 - - if not confirm and not cc.Msg.dialog(default_no=True): - return 1 - - cc.Msg.msg(f"Remove the '{port.name}' files from the system...") - - try: - prerem = rm.run_preremove() - - if prerem != 0: - cc.Msg.err(f"Execution of pre-remove script failed (code: {prerem}") - return prerem - - rm.remove() - - postrem = rm.run_postremove() - - if postrem != 0: - cc.Msg.err( - f"Execution of post-remove script failed (code: {postrem}" - ) - return postrem - - except FileNotFoundError as e: - cc.Msg.err(f"File not found: {e}") - return 1 - except ce.PortFilesBrokenError: - cc.Msg.err(f"Port '{port.name}' is broken!") - return 1 - - if not skip_check: - cc.Msg.msg("System check...") - check = cc.check(port) - - if check: - cc.Msg.err("System check error!") - # TODO: переделать поведение cc.check(). - # Пусть она возвращает dict со списком - # присутствующих и отсутствующих файлов - - return 1 - - code = 0 - if not ignore_db: - cc.Msg.msg(f"Remove port '{port.name}' from database...", endl=" ") - - db.remove() - if not db.check(): - print("OK") - else: - print("ERROR!") - code = 1 - - db.close() - - return code - return 0 diff --git a/src/cport_cli/shared.py b/src/cport_cli/shared.py deleted file mode 100644 index 6bc1329949c804166e5189519d38672ef1e70387..0000000000000000000000000000000000000000 --- a/src/cport_cli/shared.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - diff --git a/src/cport_cli/shared.sh b/src/cport_cli/shared.sh deleted file mode 100644 index 6cc7d927585f35dd369910502a00f274040f3c5f..0000000000000000000000000000000000000000 --- a/src/cport_cli/shared.sh +++ /dev/null @@ -1,114 +0,0 @@ -#!/bin/bash -e -# -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -set +h -umask 022 - -CALM="/mnt/calm" -CALM_TGT=$(uname -m)-calm-linux-gnu -LC_ALL=POSIX -PATH=$CALM/tools/bin:$PATH -CONFIG_SITE=$CALM/usr/share/config.site - -export CALM CALM_TGT LC_ALL PATH CONFIG_SITE - -# Log messages -# Usage: log_msg [message] [status] -function log_msg() { - # shellcheck disable=SC2046 - if [ $(whoami) != "root" ]; then - log_file="$HOME/.cache/cport.log" - else - log_file="/var/log/cport.log" - fi - msg="[ `LC_ALL=C date` ] [ $2 ] [ from 'install' file ] $1" - echo "$msg" >> $log_file -} - -# Usage: $1 port -function configuration_log_msg() { - log_msg "[Step 1] Port configuration" "info" -} - -function make_log_msg() { - log_msg "[Step 2] Performing a Port Assembly" "info" -} - -function install_log_msg() { - log_msg "[Step 3] Performing a port install" "info" -} - -function setup_log_msg() { - log_msg "[Step 4] Performing a port setup" "info" -} - -# Usage: -# dialog_msg [default_no = 1 or 0] -function dialog_msg() { - echo -e -n "\nContinue? " - if [ $1 == "0" ]; then - echo -n "[y/N] " - else - echo -n "[Y/n] " - fi - - read run - - if [ $run == "n" ]; then - return 1 - elif [ $run == "y" ]; then - return 0 - else - echo "Unknown input!" - return dialog_msg - fi -} - -function msg() { - echo -n -e "\e[1m==>\e[0m $1" - log_msg $1 "info" -} - -function err_msg() { - echo -n -e "[\e[1;31m!\e[0m] $1" - log_msg $1 "fail" -} - -function warn_msg() { - echo -n -e "[\e[1;33mW\e[0m] $1" - log_msg $1 "warn" -} - -function ok_msg() { - echo -n -e "[\e[1;32m✓\e[0m] $1" - if [ $2 == "0" ]; then - log_msg $1 " ok " - fi -} - -function warn_header() { - echo -e "\n\n\a\e[1;33m--==\e[0m \e[31mWARNING!\e[0m \e[1;33m==--\e[0m\n$1" -} - -function err_header() { - echo -e "\n\n\a\e[1;31m--==\e[0m \e[31mERROR!\e[0m \e[1;31m==--\e[0m\n$1" -} - -function note_header() { - echo -e "\n\n\a\e[1;32m--==\e[0m \e[1;33mNOTE!\e[0m \e[1;32m==--\e[0m\n$1" -} diff --git a/src/cport_cli/update.py b/src/cport_cli/update.py deleted file mode 100644 index e9950f69fc2c0e39f3836f85440debeb88f1d52d..0000000000000000000000000000000000000000 --- a/src/cport_cli/update.py +++ /dev/null @@ -1,158 +0,0 @@ -#!/usr/bin/python3 -# -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from shutil import get_terminal_size -from urllib.error import URLError - -import toml - -import libcport.core as cc -from libcport.constants import CACHE_UNPACKED_DIR -from libcport.constants import CALMIRA_RELEASE -from libcport.datatypes import port -from libcport.update import Update - - -def check() -> int: - """ - Принцип работы `check()`: - - - Скачивание архива системы портов из репозитория; - - Распаковка архива системы портов в кеш (`CACHE_UNPACKED_DIR`); - - Получение номера версии Calmira GNU/Linux-libre; - - Сравнение версии дистрибутива Calmira и релиза, указанного в метаданных СП: - - если версия установленного дистрибутива не содержится в метаданных СП, прервать обновление; - - Получение списка изменений: - - все изменения; - - добавленные порты; - - удалённые порты; - - обновлённые порты. - - Формирование списка изменений в том случае, если хотя бы один из списков >= 1: - - вывод всего, что добавлено; - - вывод всего, что удалено; - - вывод обновлений (указание старой версии - которая в установленной СП, указаное новой версии из новой СП); - - Обновление системы портов: - - Удаление существующей СП в `PORTS_DIR`; - - Копирование скачанной распакованной СП в `PORTS_DIR`; - """ - - u = Update(CACHE_UNPACKED_DIR) - try: - cc.Msg.msg("Downloading the port system...") - u.get() - except URLError: - cc.Msg.err("Temporary failure in name resolution!") - return 1 - - cc.Msg.msg(f"Unpacking ports in '{CACHE_UNPACKED_DIR}'...") - u.unpack() - - try: - rel = u.check_release() - except KeyError: - cc.Msg.err(f"File '{CALMIRA_RELEASE}' is not valid file!") - return 1 - - if not rel: - ports_rel_metadata = u._get_metadata(mode=1)['system']['release'] - sys_rel = cc.get_calm_release() - - ports_rel = "" - for i in ports_rel_metadata: - ports_rel += str(i) + " " - - print(f"The port system is for version: {ports_rel}") - print(f"Version system installed: {sys_rel}") - err_msg = "The ports system is not compatible with the current " \ - "release of Calmira GNU/Linux(-libre)!" - cc.Msg.err(err_msg) - - return 1 - - cc.Msg.msg("Generating list of changes...") - - try: - added = u.get_added_ports() - removed = u.get_removed_ports() - updates = u.get_updated_ports() - other = u.get_other_ports() - except KeyError: - cc.Msg.err( - "Error parsing the metadata of the installed or updated ports system" - ) - return 1 - - width = get_terminal_size()[0] - - if width < 80: - cc.Msg.warn("The terminal is very small! Required width: 80 or more chars.") - return 1 - - if len(added) > 0 or len(removed) > 0 or len(updates) > 0 or len(other) > 0: - pass - else: - cc.Msg.err("No updates.") - return -1 - - if len(added) > 0 or len(removed) > 0: - print("\n\033[1mAdditions/deletions:\033[0m") - print("| {0:58} | {1:15} |".format("name", "status")) - print("|{0}|{1}|".format(60 * '-', 17 * '-')) - - if len(added) > 0: - for i in added: - print("| {0:58} | {1:15} |".format(str(i), "added")) - if len(removed) > 0: - for i in removed: - print("| {0:58} | {1:15} |".format(str(i), "removed")) - - if len(updates) > 0: - print("\n\033[1mUpgrades/downgrades:\033[0m") - print( - "| {0:40} | {1:15} | {2:15} |".format( - "name", "old version", "new version" - ) - ) - print("|{0}|{1}|{1}|".format(42 * '-', 17 * '-')) - - for change in updates: - name = change['name'] - vers = change['version'] - - db = cc.CDatabase(port(name)) - if db.check(): - name = f"[i] {name}" - - print( - "| {0:40} | {1:15} | {2:15} |".format( - name, vers['old'], vers['new'] - ) - ) - - if len(other) > 0: - print("\n\033[1mOther changes:\033[0m") - print("| {0:58} | {1:15} |".format("name", "version")) - print("|{0}|{1}|".format(60 * '-', 17 * '-')) - - for name in other: - port_name = port(name) - data = toml.load(f"{port_name.path}/port.toml") - vers = data['package']['version'] - print("| {0:58} | {1:15} |".format(name, vers)) - - return 0 diff --git a/src/cport_cli/upgrade.py b/src/cport_cli/upgrade.py deleted file mode 100644 index a6ec5ea5ad0ebacfeceb0ef6ed3c15b26067277a..0000000000000000000000000000000000000000 --- a/src/cport_cli/upgrade.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/python3 -# -# 'upgrade.py' -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import cport_cli.install as ci -import cport_cli.remove as cr -import libcport.core as cc -import libcport.exceptions as ce -import toml -from libcport.datatypes import port - - -def check(name: port) -> bool: - # TODO: добавить проверку соответствия версии установленного - # порта и порта в СП - - db = cc.CDatabase(name) - code = db.check() - db.close() - - return code - - -def upgrade_soft(name: port) -> int: - db = cc.CDatabase(name) - if not db.check(): - cc.Msg.err(f"Error: port '{name.name}' not in database!") - return 1 - - db.remove() - db.close() - - run = ci.install(name, False, False) - - if run != 0: - return run - return 0 - - -def upgrade_hard(name: port) -> int: - if not check(name): - cc.Msg.err(f"Error: port '{name.name}' not in database!") - return 1 - - run = cr.remove(name) - - if run != 0: - return run - - run = ci.install(name, False, False) - - if run != 0: - return run - return 0 - - -def main(args: list[str]) -> int: - for arg in args: - try: - arg = port(arg) - data = toml.load(f"{arg.path}/port.toml") - upgrade_mode = data['package']['upgrade_mode'] - - if upgrade_mode == "soft": - run = upgrade_soft(arg) - elif upgrade_mode == "hard": - run = upgrade_hard(arg) - else: - cc.Msg.err(f"Port '{arg.name}' is broken:" \ - f" 'package.upgrade_mode' is invalid: '{upgrade_mode}'") - cc.Msg.err("Valid names: soft & hard") - return 1 - - if run != 0: - return run - except ce.PortNotFoundError: - cc.Msg.err(f"Error: port '{arg}' not found!") - return 1 - except toml.decoder.TomlDecodeError: - cc.Msg.err("Error decoding the configuration file of the port" \ - f" '{arg}'!") - return 1 - return 0 diff --git a/src/libcport/__init__.py b/src/libcport/__init__.py deleted file mode 100644 index 8d1c8b69c3fce7bea45c73efd06983e3c419a92f..0000000000000000000000000000000000000000 --- a/src/libcport/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/libcport/constants.py b/src/libcport/constants.py deleted file mode 100644 index 3d864dca4e0e9db859ea529a31f0960266285808..0000000000000000000000000000000000000000 --- a/src/libcport/constants.py +++ /dev/null @@ -1,91 +0,0 @@ -#!/usr/bin/python -# -# constants.py - base constants, variables and settings from the -# '/etc/cport.toml' file for cport -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -import toml - -home_dir = os.environ.get('HOME') - -BASE_CONFIG = { - "repo": { - "url": "https://gitlab.com/calmiralinux/cabs/Ports", - "branch": "testing" - }, - "ports": { - "path": "/usr/ports", - "werror": "no" - }, - "build_system": { - "use_cache": "yes", - "resolve_deps": "no" - } -} - -if not os.path.exists('/etc/cport.conf'): - print("-- cport kernel -- create initial config...") - try: - with open('/etc/cport.conf', 'w') as f: - toml.dump(BASE_CONFIG, f) - except PermissionError: - print("-- cport kernel -- Permission denied!") - -data = toml.load('/etc/cport.conf') - -# CONSTANTS -NAME: str = "cport" -VERSION: str = "1.0current_beta" -PORTS_DIR: str = data['ports'].get('path') -CACHE_DIR: str = "/var/cache/cport" -CACHE_DOWNLOADED_DIR: str = f"{CACHE_DIR}/downloads" -CACHE_UNPACKED_DIR: str = "/usr/src" -CONF_FILE: str = "/etc/cport.conf" -LIB_DIR: str = "/var/lib/Calmira" -LOCAL_LIB_DIR: str = f"{home_dir}/.local/share/Calmira" -DATABASE_FILE: str = f"{LIB_DIR}/software.db" -DATABASE_USER_FILE: str = f"{LOCAL_LIB_DIR}/software.db" -METADATA_FILE: str = f"{PORTS_DIR}/metadata.toml" -CALMIRA_RELEASE: str = "/etc/calm-release" -LOG_FILE: str = "/var/log/cport.log" -USER_LOG_FILE: str = f"{home_dir}/.cache/cport.log" - -UPDATE_BRANCH: str = data['repo'].get('branch') -PORTS_REPO_URL: str = data['repo'].get('url') -WERROR: bool = data['ports'].get('werror') -USE_CACHE: str = data['build_system'].get('use_cache') -RESOLVE_DEPS: str = data['build_system'].get('resolve_deps') - -PORT_TOML_PARAMS: dict = { - "package": [ - "name", "version", "description", "maintainer", - "releases", "priority", "usage", "upgrade_mode", - "build_time", "description" - ], - "deps": [ - "required", "recommend", "optional" - ], - "port": [ - "url", "md5", "sha256", "file" - ] -} -# Статусы секций: обязательная (True) и необязательная (False) -PORT_TOML_STATUSES: dict = { - "package": True, - "deps": False, - "port": False -} diff --git a/src/libcport/core.py b/src/libcport/core.py deleted file mode 100644 index 505fd199a8bef75825d12c8ecd2286eb956adb3c..0000000000000000000000000000000000000000 --- a/src/libcport/core.py +++ /dev/null @@ -1,520 +0,0 @@ -#!/usr/bin/python3 -# -# core.py - core objects for cport package manager -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -""" -'core.py' is the main cport source code that is the core of this package -manager. Contains basic functionality for getting information about the Calmira -GNU/Linux-libre distribution (required to check the port for compatibility with -the current release of Calmira). - -Tasks solved by this module: - -* Obtaining information about the installed system; -* Checking for files belonging to a specific port; -* More high-level interaction with the database; -""" - -import libcport.datatypes as cd -import libcport.exceptions as ce -import libcport.constants as cc - -import os -import sys -import toml -import time -import shutil -import sqlite3 - - -def dbg_msg(msg: str, endl="\n") -> None: - # CPORT_DBG='yes' or CPORT_DBG='no' or CPORT_DBG is not set - cport_dbg: str | None = os.environ.get('CPORT_DBG') - - if cport_dbg is not None and cport_dbg == "yes": - print(f"[DEBUG] {msg}", end=endl) - - with open('.cport.debug.log', 'a') as f: - msg = fr"{msg}" - f.write(f"{msg}\n") - - -def ckuser() -> bool: - """Checks if cport is running as root. - If running, returns True, otherwise - returns False.""" - - return os.geteuid() == 0 - - -def get_calm_release() -> str: - data = toml.load(cc.CALMIRA_RELEASE) - return data['system']['version'] - - -def check(name: cd.port) -> int | None: - """Checks if a port is installed to the system or not. - - Statuses: - - 0 - 100% files; - - 1 - 75-99% files; - - 2 - 50-74% files; - - 3 - 5-49% files; - """ - - files_list = f"{name.path}/files.list" - fcount = len(files_list) - f_install_count = 0 - - with open(files_list) as f: - dbg_msg(f"[check] open '{files_list}'...") - files = f.read().splitlines() - - for file in files: - if not os.path.exists(file): - dbg_msg(f"[files checking...] File '{file}':\033[31m not found\033[0m") - else: - f_install_count += 1 - - if f_install_count == fcount: - return 0 - elif f_install_count >= (fcount / 0.75): - return 1 - elif f_install_count >= (fcount / 0.5): - return 2 - elif f_install_count >= (fcount / 0.05): - return 3 - - -def p_lint(name: str) -> bool: - """Ports linter. Checks that the `port.toml` and - `files.list` files of the port are correct""" - - try: - path = cd.port(name).path - except ce.PortBrokenError: - return False - except ce.PortNotFoundError: - return False - - ret_code = True - - # Checking the "port.toml" file - try: - dbg_msg("[p_lint] checking the 'port.toml' file") - - data = toml.load(f"{path}/port.toml") - - # Проверка наличия секций "package" и "port" - if data.get('package') is None or data.get('port') is None: - raise ce.FilePortTomlError - - # Проверка наличия необходимых параметров в секции "package" - # Параметры в секции "port" являются необязательными - for param in cc.PORT_TOML_PARAMS["package"]: - if data["package"].get(param) is None: - raise ce.FilePortTomlError - - except toml.TomlDecodeError: - raise ce.FilePortTomlError - except FileNotFoundError: - raise ce.FilePortTomlError - - # Checking the "files.list" file - try: - dbg_msg("Checking the 'files.list' file") - dbg_msg(f"{path}/files.list") - - with open(f"{path}/files.list") as f: - flist = f.read().splitlines() - - if len(flist) < 1: - raise ce.FileFilesListError - - except FileNotFoundError: - raise ce.FileFilesListError - - return ret_code - - -def get_files_in_cache() -> int: - dirs = (cc.CACHE_UNPACKED_DIR, cc.CACHE_DOWNLOADED_DIR) - files = 0 - - for _dir in dirs: - dbg_msg(f"[get_files_in_cache] work directory: '{_dir}'") - - fls = os.listdir(_dir) - files += len(fls) - for _ in os.walk(_dir): - files += 1 - - dbg_msg(f"Files count: {files}") - return files - - -def wipe_cache() -> None: - """This method wipes all data in the cache""" - - dirs = (cc.CACHE_UNPACKED_DIR, cc.CACHE_DOWNLOADED_DIR) - - for _dir in dirs: - dbg_msg(f"Clearing the '{_dir}' directory...") - fls = os.listdir(_dir) - for file in fls: - file = f"{_dir}/{file}" - dbg_msg(f"Detect the '{file}' file") - if os.path.isfile(file): - os.remove(file) - dbg_msg("This is file. Removed") - elif os.path.isdir(file): - shutil.rmtree(file) - dbg_msg("This is a dir. Removed") - - -class CDatabase(object): - - def __init__(self, name: cd.port, dbfile: str = cc.DATABASE_FILE): - self.name = name - - dbg_msg(f"Работа с БД по порту '{name.name}'") - - with open(f"{name.path}/port.toml") as f: - self.port_data = toml.load(f) - - self.con = sqlite3.connect(dbfile) - self.cur = self.con.cursor() - - def close(self) -> None: - self.cur.close() - - def create_initial_db(self) -> None: - queries = """CREATE TABLE ports_in_fs ( - name TEXT, version TEXT, description TEXT, maintainer TEXT, - releases TEXT, priority TEXT, usage REAL, upgrade_mode TEXT, - build_time REAL - ); - CREATE TABLE installed_ports ( - name TEXT, version TEXT, description TEXT, maintainer TEXT, - releases TEXT, priority TEXT, usage REAL, upgrade_mode TEXT, - build_time REAL, status INT, build_date REAL - );""" - - self.cur.executescript(queries) - - def add(self, status: int = 0, table: str = "installed_ports") -> None: - """Statuses: - - 0 - 100% files; - - 1 - 75-99% files; - - 2 - 50-74% files; - - 3 - 5-49% files; - """ - - conf = self.port_data - params = ( - 'version', 'description', - 'maintainer', 'releases', 'priority', - 'usage', 'upgrade_mode', 'build_time' - ) - - data = [self.name.name] - - for prm in params: - tp_str = "" - # The next step is to check the received data. In the event that the - # data type 'list' is received, then it is converted to a 'str' - if type(conf['package'].get(prm)) == list: - for i in conf['package'][prm]: - tp_str += str(i) + " " - else: - tp_str = conf['package'].get(prm) - data.append(tp_str) - - data.append(status) # Status - - if table == "installed_ports": - data.append(time.time()) - vlist = "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - else: - vlist = "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - - db_query = f"INSERT INTO {table} VALUES {vlist}" - self.cur.execute(db_query, data) - self.con.commit() - - def remove(self, table: str = "installed_ports") -> None: - name = self.name.name - self.cur.execute(f"DELETE FROM {table} WHERE name = '{name}'") - self.con.commit() - - def get_one(self, table: str = "installed_ports"): - name = self.name.name - dt = self.cur.execute(f"SELECT * FROM {table} WHERE name = ?", (name,)) - - return dt.fetchone() - - def get_all(self, table: str = "installed_ports"): - """Warning: this method is too slow!""" - - name = self.name.name - dt = self.cur.execute(f"SELECT * FROM {table} WHERE NAME = ?", (name,)) - - return dt.fetchall() - - def check(self, table: str = "installed_ports") -> bool: - query = f"SELECT * FROM {table} WHERE NAME = ?" - dt = self.cur.execute(query, (self.name.name,)) - - return dt.fetchone() is not None - - def execute(self, query: str, params: tuple, commit: bool = False): - data = self.cur.execute(query, params) - - if commit: - self.con.commit() - - return data.fetchall() - - def gen_all_ports(self) -> None: - metadata = toml.load(cc.METADATA_FILE) - ports = metadata['port_sys']['ports'] - - self.cur.execute("DELETE FROM ports_in_fs") - - for port in ports: - port = cd.port(port) - port_conf = toml.load(f"{port.path}/port.toml") - data = [port.name] - params = ( - 'version', 'description', 'maintainer', - 'releases', 'priority', 'usage', - 'upgrade_mode', 'build_time' - ) - - for param in params: - tp_str = "" - if type(port_conf['package'].get(param)) == list: - # Converting list to str - for i in port_conf['package'][param]: - tp_str += str(i) + " " - else: - tp_str = port_conf['package'].get(param) - data.append(tp_str) - - db_query = "INSERT INTO ports_in_fs VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" - self.cur.execute(db_query, data) - self.con.commit() - - -class CList(object): - - def __init__(self): - self.md = toml.load(cc.METADATA_FILE) - self.ports = self.md['port_sys']['ports'] - - def all(self) -> list[str]: - return self.ports - - def installed(self) -> list[tuple[str, float]]: - """Returns list of tuples with values: - - (port name, installation date) - - Types: - - (str, float) - - The installation time is returned in UNIX Time format. - """ - - ports_list = [] - - for port in self.ports: - dbg_msg(f"Selected port '{port}'...") - - try: - db = CDatabase(cd.port(port)) - - if db.check(): - dbg_msg("This port is installed") - - data = db.get_one() - ports_list.append((data[0], data[-1])) - - db.close() - except ce.PortNotFoundError: - dbg_msg(f"Port '{port}' not found") - continue - except ce.PortBrokenError: - dbg_msg(f"Port '{port}' is broken") - continue - - return ports_list - - def not_installed(self) -> list[str]: - ports_list = [] - for port in self.ports: - try: - db = CDatabase(cd.port(port)) - if not db.check(): - db.close() - ports_list.append(port) - db.close() - except ce.PortNotFoundError: - dbg_msg(f"Port '{port}' not found") - continue - except ce.PortBrokenError: - dbg_msg(f"Port '{port}' is broken") - continue - return ports_list - - def broken(self) -> list[str]: - ports_list = [] - for port in self.ports: - try: - cd.port(port) - except ce.PortBrokenError: - ports_list.append(port) - return ports_list - - -def rebuild() -> None: - cl = CList() - ports = cl.all() - - for port in ports: - try: - port = cd.port(port) - db = CDatabase(port) - except ce.PortNotFoundError: - dbg_msg(f"-- cport kernel -- port '{port}' not found in the filesystem") - continue - - files_list_file = f"{port.path}/files.list" - - if not os.path.exists(files_list_file): - dbg_msg(f"File '{files_list_file}' of port '{port.name}' not found") - continue - - if db.check(): - dbg_msg(f"port '{port.name}' already in DB! Skipping...") - continue - - with open(files_list_file) as f: - files = f.read().splitlines() - - files_count = 0 - - for file in files: - if os.path.exists(file): - files_count += 1 - - if files_count == len(files): - port_status = 0 - elif len(files) / 0.75: - port_status = 1 - elif files_count >= (len(files) / 0.5): - port_status = 2 - elif files_count >= (len(files) / 0.05): - port_status = 3 - else: - port_status = 4 - - if port_status == 4: - db.close() - continue - - db.add(status=port_status) - db.close() - - -def log_msg(msg: str, status: str = "info") -> None: - file = cc.LOG_FILE if ckuser() else cc.USER_LOG_FILE - - with open(file, 'a') as f: - msg = f"[ {time.ctime()} ] [ {status} ] {msg}" - f.write(msg) - - -class Msg: - - @staticmethod - def log_msg(msg: str, status: str = "info") -> None: - file = cc.LOG_FILE if ckuser() else cc.USER_LOG_FILE - - with open(file, 'a') as f: - msg = f"[ {time.ctime()} ] [ {status} ] {msg}" - f.write(msg) - - @staticmethod - def dialog(default_no: bool = False) -> bool: - print("\n:: Continue?", end=" ") - if default_no: - print("[y/N]", end=" ") - else: - print("[Y/n]", end=" ") - run = input() - if run == "N" or run == "n": - return False - elif run == "Y" or run == "y": - return True - elif run == "": - if default_no: - Msg.warn("I choose the negative option (N)!", log=False) - return False - else: - Msg.warn("I choose the affirmative option (Y)!", log=False) - return True - else: - print("Unknown input!") - return Msg.dialog(default_no) - - @staticmethod - def msg(msg: str, startl="", endl="\n", log: bool = True) -> None: - print(f"{startl}\033[1m==>\033[0m {msg}", end=endl) - if log: - log_msg(f"{startl}{msg}{endl}") - - @staticmethod - def err(msg: str, startl="", endl="\n", log: bool = True) -> None: - print(f"{startl}[\033[1m\033[31m!\033[0m] {msg}", file=sys.stderr, end=endl) - if log: - log_msg(f"{startl}{msg}{endl}", status="fail") - - @staticmethod - def warn(msg: str, startl="", endl="\n", log: bool = True) -> None: - print(f"{startl}[\033[33mW\033[0m] {msg}", file=sys.stderr, end=endl) - if log: - log_msg(f"{startl}{msg}{endl}", status="warn") - - @staticmethod - def ok(msg: str, startl="", endl="\n", log: bool = True) -> None: - print(f"{startl}[\033[32m✓\033[0m] {msg}", end=endl) - if log: - log_msg(f"{startl}{msg}{endl}", status=" ok ") - - @staticmethod - def header(msg: str, color: str = "\033[1m"): - try: - term_size = os.get_terminal_size()[0] - msg = f" {msg} " # В пробелах для того, чтобы выглядело нормально - hdr_msg = msg.center(term_size, '=') - except BrokenPipeError: - hdr_msg = f"--== {msg} --==" - - print(f"{color}{hdr_msg}\033[0m") - log_msg(f"{msg}\n", status="info") diff --git a/src/libcport/datatypes.py b/src/libcport/datatypes.py deleted file mode 100644 index 24507b68a3899f833802bef4b7d5a8b64b9a929e..0000000000000000000000000000000000000000 --- a/src/libcport/datatypes.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/python3 -# -# datatypes.py - the 'port' data type for Python -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -from dataclasses import dataclass -from dataclasses import field - -from libcport.constants import PORTS_DIR -from libcport.exceptions import PortBrokenError -from libcport.exceptions import PortNotFoundError - - -@dataclass -class port: - """ - Usage: - name: libcport.datatypes.port - - Data: - * name - port name; - * path - path to port's files ('/usr/ports/...'); - * files - список файлов из path; - """ - - name: str - path: str = field(init=False) - files: list = field(init=False) - - def __post_init__(self): - self.path = f"{PORTS_DIR}/{self.name}" - - if not os.path.isdir(self.path): - raise PortNotFoundError - - self.is_found = True - self.files = os.listdir(self.path) - files = ('install', 'port.toml') # TODO: in the stable version return file 'files.list' - - for file in files: - if file not in self.files: - raise PortBrokenError(file) diff --git a/src/libcport/dependencies.py b/src/libcport/dependencies.py deleted file mode 100644 index 26fb7c065bb0c616fb45a8af289607904da9aece..0000000000000000000000000000000000000000 --- a/src/libcport/dependencies.py +++ /dev/null @@ -1,157 +0,0 @@ -#!/usr/bin/python3 -# -# dependencies.py -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from typing import Any - -import libcport.datatypes as cd -import libcport.core as cc -import libcport.exceptions as ce -import libcport.info as ci - - -class CDependencyGenerator: - - """Получает список всех зависимостей указанного порта""" - - def __init__(self, port: cd.port): - self.port = port - - info = ci.CPortInfo(port) - self.port_deps = info.deps() - - self.dependency_list = {} - - def _deps(self, port: cd.port = None) -> list[str]: - if port is None: - # Подразумевается, что в данном случае - # используется имя искомого порта - - return self.port_deps.get('required') - - p_info = ci.CPortInfo(port) - p_deps = p_info.deps() - - return p_deps.get('required') - - def deplist(self) -> dict: - """Gets a list of all dependencies of the specified ports - - Return value: - - >>> a = CDependencyGenerator(cd.port('base/bash')) - >>> a.deplist() - {'base/bash': ['base/binutils', 'base/bison', 'base/coreutils', ...], 'base/binutils': ['base/coreutils', - 'base/diffutils', ...], 'base/bison': [...]} - """ - - deps = self._deps() - if not deps: - return {} - - for dep in deps: - try: - dependency_list = self._deps(cd.port(dep)) - if dependency_list is not None: - self.dependency_list[dep] = dependency_list - except ce.PortNotFoundError: - cc.dbg_msg(f"[CDependencyResolve.deplist] port '{dep}' not found") - - return self.dependency_list - - -class CDependencyResolver(CDependencyGenerator): - - """Форматирует список всех зависимостей порта""" - - def __init__(self, port: cd.port): - super().__init__(port) - self.dependency_form_list = [] - - def _append(self, portname: str) -> None: - """Adds ports to the list if all conditions are true: - - * The port is not yet in the list; - * The port name to add is not the source port name; - * The port to add is not in the 'installed' table of the cport database;""" - - try: - db = cc.CDatabase(cd.port(portname)) - except ce.PortNotFoundError: - return - - data = all([ - portname not in self.dependency_form_list, - portname != self.port.name, - not db.check() - ]) - - if data: - self.dependency_form_list.append(portname) - - def gen_deplist(self) -> list: - """Generates a list of dependencies of the specified ports - - Return value: - - >>> a = CDependencyResolver(cd.port('base/bash')) - >>> a.gen_deplist() - ['base/shadow', 'base/openssl', 'base/m4', 'base/perl', 'base/readline', 'base/ncurses', 'base/glibc'] - """ - - deplist = self.deplist() - - for port in deplist.keys(): - for subport in deplist[port]: - if subport not in self.dependency_form_list: - self._append(subport) - self._append(port) - - return self.dependency_form_list[::-1] - - -def check_conflicts(port1: cd.port, port2: cd.port) -> dict[str, list[Any] | bool]: - """Performs conflict checking between two ports. Return value: - - >>> check_conflicts(cd.port("base/acl"), cd.port("base/attr")) - { - "status": True, - "conflict_files": ["/usr/bin/foo", "/usr/share/foo"] - } - - If there are no conflicts, then 'status' = False, and the - 'conflict_files' list is empty. Otherwise, 'status' = True, - and 'conflict_files' contains a list of conflicting files. - """ - - with open(f"{port1.path}/files.list") as f: - port1_flist = f.read().splitlines() - with open(f"{port2.path}/files.list") as f: - port2_flist = f.read().splitlines() - - data = { - "status": False, - "conflict_files": [] - } - - for file in port1_flist: - if file in port2_flist: - cc.dbg_msg(f"Conflict: \n\t{port1.name} and {port2.name}:\n\t\t{file}") - data['status'] = True - data['conflict_files'].append(file) - - return data diff --git a/src/libcport/download.py b/src/libcport/download.py deleted file mode 100644 index 6ff903e095d5c393319c3934d4d4260643766bf1..0000000000000000000000000000000000000000 --- a/src/libcport/download.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/python3 -# -# download.py -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -import shutil -import tempfile -import urllib.parse -import urllib.request - -import libcport.core as cc - - -def fname_from_url(url: str) -> str | None: - fname = os.path.basename(urllib.parse.urlparse(url).path) - if len(fname.strip(" \n\t.")) == 0: - return None - return fname - - -def fname_from_headers(headers) -> str | None: - if type(headers) == str: - headers = headers.splitlines() - if type(headers) == list: - headers = dict([x.split(':', 1) for x in headers]) - - cdisp = headers.get("Content-Disposition") - if not cdisp: - return None - - cdtype = cdisp.split(";") - if len(cdtype) == 1: - return None - - if cdtype[0].strip().lower() not in ('inline', 'attachment'): - return None - - fnames = [x for x in cdtype[1:] if x.strip().startwith('filename=')] - if len(fnames) > 1: - return None - - name = fnames[0].split('=')[1].strip(' \t"') - name = os.path.basename(name) - - if not name: - return None - return name - - -def detect_filename(url: str, tgt: str = None, headers: list = None) -> str: - names = dict(tgt='', url='', headers='') - if tgt: - names['tgt'] = tgt or '' - if url: - names['url'] = fname_from_url(url) or '' - if headers: - names['headers'] = fname_from_headers(headers) or '' - return names['tgt'] or names['headers'] or names['url'] - - -def download(url: str, tgt: str = None) -> str: - outdir = None - if tgt and os.path.isdir(tgt): - outdir = tgt - - # get filename from temp file in current dir. - prefix = detect_filename(url, tgt) - fd, tmpfile = tempfile.mkstemp(".tmp", prefix=prefix, dir=".") - os.close(fd) - os.unlink(tmpfile) - - binurl = list(urllib.parse.urlsplit(url)) - binurl[2] = urllib.parse.quote(binurl[2]) - binurl = urllib.parse.urlunsplit(binurl) - - tmpfile, headers = urllib.request.urlretrieve(binurl, tmpfile) - fname = detect_filename(url, tgt, headers) - - cc.dbg_msg(f"{url=}\n{headers=}\n\n{fname=}\n{tmpfile=}") - - shutil.move(tmpfile, fname) - - return fname diff --git a/src/libcport/exceptions.py b/src/libcport/exceptions.py deleted file mode 100644 index 774fdccffc9dd86280ee9cda8820357d24b17dec..0000000000000000000000000000000000000000 --- a/src/libcport/exceptions.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/python3 -# -# exceptions.py - base exceptions for cport package manager -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -class PortError(Exception): - - def __init__(self, value: str = None): - self.value = value - - def __str__(self): - return self.value - - -class CSystemError(PortError): - - def __init__(self, value: str = None): - self.value = value - - def __str__(self): - return self.value - - -class PortNotFoundError(PortError): - - def __init__(self, value=None): - self.value = value - - def __str__(self): - return self.value - - -class PortBrokenError(PortError): - - def __init__(self, value: str = None): - self.value = value - - def __str__(self): - return self.value - - -class PortFilesBrokenError(PortError): - - def __init__(self, value=None): - self.value = value - - def __str__(self): - return self.value - - -class FilePortTomlError(PortFilesBrokenError): - - def __init__(self, value=None): - self.value = value - - def __str__(self): - return self.value - - -class FileFilesListError(PortFilesBrokenError): - - def __init__(self, value=None): - self.value = value - - def __str__(self): - return self.value diff --git a/src/libcport/info.py b/src/libcport/info.py deleted file mode 100644 index 68b47dd956dcb335d23401f873e5a060b20eceeb..0000000000000000000000000000000000000000 --- a/src/libcport/info.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/python3 -# -# info.py - get information about ports -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import os -import toml - -import libcport.core as cc -import libcport.datatypes as cd -import libcport.exceptions as ce - - -class CPortInfo: - - """Get data from the 'port.toml' file of the selected port""" - - def __init__(self, port: cd.port): - self.port: cd.port = port - conf_file: str = f"{port.path}/port.toml" - - if not os.path.isfile(conf_file): - raise ce.FilePortTomlError - - self.data: dict = toml.load(conf_file) - - def package(self) -> dict: - """Return all data from the '[package]' section (port.toml file) + installed status (True or False) - - Returned value: - - { - "name": str, - "version": str, - "description": str, - "maintainer": str, - "releases": list[str], - "priority": str, - "usage": float, - "upgrade_mode": str, - "build_time": float, - "installed": bool - } - """ - - db = cc.CDatabase(self.port) - - installed: bool = db.check() - - db.close() - - self.data['package']['installed'] = installed - return self.data['package'] - - def deps(self) -> dict: - """Return all data from the '[deps]' section - - Return value: - - { - "required": str | None, - "recommend": str | None, - "optional": str | None - } - """ - - return self.data.get('deps') - - def port(self) -> dict: - """Return all data from the '[port]' section - - Return value: - - { - "url": str | None, - "file": str | None, - "md5": str | None, - "sha256": str | None - } - """ - - return self.data['port'] - - -def version_compare(ver_first: str, ver_second: str) -> bool: - """Function to compare two versions - - :arg ver_first - first software version; - :arg ver_second - second software version; - - If they are equal, True is returned. Otherwise, there is a check that - ver_first > ver_second. Accordingly, True or False is returned. - - NOTE: only works with versions that consist only of numbers: "INT.INT" - """ - - v1_n = ver_first.split('.') - v2_n = ver_second.split('.') - - for v1, v2 in zip(v1_n, v2_n): - if int(v1) == int(v2): - continue - return int(v1) > int(v2) - - return ver_first > ver_second diff --git a/src/libcport/install.py b/src/libcport/install.py deleted file mode 100644 index 2286d1629a5ccb8f271cc81f8d6eb52490598480..0000000000000000000000000000000000000000 --- a/src/libcport/install.py +++ /dev/null @@ -1,134 +0,0 @@ -#!/usr/bin/python3 -# -# install.py - module with functions for build, install and check ports -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import libcport.datatypes as cd -import libcport.constants as ccc -from libcport.core import dbg_msg - -import os -import toml -import wget -import tarfile -import zipfile -import subprocess -import hashlib - - -class CInstall: - - def __init__(self, port: cd.port): - self.port = port - self.data = toml.load(f"{port.path}/port.toml") - - # Optional parameters in the '[port]' section - # Type: str | None - self.url = self.data['port'].get('url') - self.md5 = self.data['port'].get('md5') - self.sha256 = self.data['port'].get('sha256') - self.fname = self.data['port'].get('file') - - def check_fname(self) -> bool: - dbg_msg(f"[check_fname()]: URL: {self.url}") - - if self.url is None or self.url == "none": - return False - - return True - - def get_fname(self) -> str: - """Method for getting the name of the file that will be downloaded from - the link from the 'port.url' parameter. The archive name is calculated - from the data in this parameter.""" - - if self.fname is not None: - return self.fname - - return wget.detect_filename(self.url) - - def download(self) -> bool: - if self.url is None or self.url == "none": - return True - - file = f"{ccc.CACHE_DOWNLOADED_DIR}/{self.get_fname()}" - - if ccc.USE_CACHE == "yes" and os.path.exists(file): - return True - - wget.download(self.url, ccc.CACHE_DOWNLOADED_DIR) - print() - - return os.path.isfile(file) - - def cksum(self) -> bool: - fname = f"{ccc.CACHE_DOWNLOADED_DIR}/{self.get_fname()}" - - dbg_msg(f"Checking the '{fname}' file...") - dbg_msg(f"URL: {self.url}") - - if self.sha256 is not None: - dbg_msg("Use the sha256") - - _hash = hashlib.sha256() - tgt = self.sha256 - else: # self.md5 exist; self.sha256 - doesn't exist - dbg_msg("Use the md5") - assert self.md5 is not None - - _hash = hashlib.md5() - tgt = self.md5 - - if not os.path.isfile(fname): - raise FileNotFoundError - - with open(fname, 'rb') as f: - # The file is read in blocks of 4 KB, since there may be a case when - # the file is large, and its "full" opening may either be incorrect - # or fill up a fairly large amount of memory, which is not - # recommended. - for blk in iter(lambda: f.read(4096), b""): - _hash.update(blk) - - return _hash.hexdigest() == tgt - - def unpack(self) -> None: - """Supported archives formats: - * tar; - * zip. - """ - - file = f"{ccc.CACHE_DOWNLOADED_DIR}/{self.get_fname()}" - - dbg_msg(f"Unpacking file '{file}'...") - - # Проверка типа файла - if zipfile.is_zipfile(file): - dbg_msg("This is zip file") - with zipfile.ZipFile(file, mode='r') as f: - f.extractall(ccc.CACHE_UNPACKED_DIR) - else: - dbg_msg("This is tar file") - with tarfile.open(file, 'r') as f: - f.extractall(path=ccc.CACHE_UNPACKED_DIR) - - def install(self, command: str = "") -> int: - build_script = f"{self.port.path}/install" - - dbg_msg(f"Build script: '{build_script}'") - - run = subprocess.run(f"{command} {build_script}", shell=True) - return run.returncode diff --git a/src/libcport/remove.py b/src/libcport/remove.py deleted file mode 100644 index b9a7d5c6de76451043f139ca6e3b83589581e322..0000000000000000000000000000000000000000 --- a/src/libcport/remove.py +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/python3 -# -# remove.py - module with functions for remove ports -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import libcport.datatypes as cd -from libcport.core import dbg_msg - -import os -import toml -import shutil -import subprocess - - -class CRemove: - - def __init__(self, port: cd.port): - self.port = port - self.port_conf = toml.load(f"{self.port.path}/port.toml") - - def check_priority(self) -> bool: - """Returned True if port has a 'system' priority - or False if port has a 'user' priority""" - - return self.port_conf['package']['priority'] == 'system' - - def files(self) -> list[str]: - fls = f"{self.port.path}/files.list" - with open(fls) as f: - data = f.read().splitlines() - - for file in data: - dbg_msg(f"File '{file}:", endl=" ") - - if os.path.exists(file): - dbg_msg("found") - else: - dbg_msg("\033[31mNOT found\033[0m") - return data - - def run_preremove(self) -> int: - prerem = f"{self.port.path}/preremove.sh" - if os.path.isfile(prerem): - dbg_msg(f"Run pre-remove script of port '{self.port.name}'") - run = subprocess.run(prerem, shell=True) - return run.returncode - return 0 - - def run_postremove(self) -> int: - postrem = f"{self.port.path}/postremove.sh" - if os.path.isfile(postrem): - dbg_msg(f"Run post-remove script of port '{self.port.name}'") - run = subprocess.run(postrem, shell=True) - return run.returncode - return 0 - - def remove(self) -> None: - for file in self.files(): - if os.path.isfile(file): - dbg_msg(f"Removing file '{file}'") - os.remove(file) - elif os.path.isdir(file): - dbg_msg(f"Removing directory '{file}'") - shutil.rmtree(file) - elif os.path.islink(file): - dbg_msg(f"Removing link '{file}'") - os.remove(file) diff --git a/src/libcport/update.py b/src/libcport/update.py deleted file mode 100644 index 634e917ac593a6dfdc23c60e10dc33894a7ea707..0000000000000000000000000000000000000000 --- a/src/libcport/update.py +++ /dev/null @@ -1,175 +0,0 @@ -#!/usr/bin/python3 -# -# update.py -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import os -import shutil -import tarfile - -import libcport.constants as ccc -import libcport.core as cc -import toml -import wget - -URL = ccc.PORTS_REPO_URL -BRANCH = ccc.UPDATE_BRANCH - - -class Check: - - def __init__(self, path: str): - self.path = path - - def _get_metadata(self, mode: int = 1) -> dict: - data = [ - ccc.PORTS_DIR, - f"{self.path}/ports" - ] - return toml.load(f"{data[mode]}/metadata.toml") - - def _get_ports(self) -> tuple[list, list]: - d0 = self._get_metadata(mode=0) - d1 = self._get_metadata(mode=1) - - return d0['port_sys']['ports'], d1['port_sys']['ports'] - - def _clean_sys(self) -> None: - try: - shutil.rmtree(f"{self.path}/ports") - os.remove(f"{ccc.CACHE_DOWNLOADED_DIR}/ports.txz") - except FileNotFoundError: - pass - - def check_release(self) -> bool: - rel = cc.get_calm_release() - ports_rel = self._get_metadata(mode=1)['system']['release'] - - return rel in ports_rel - - def get_added_ports(self) -> list[str]: - ports_inst = self._get_ports()[0] - ports_dwnl = self._get_ports()[1] - changes = [] - - for port in ports_dwnl: - if port not in ports_inst: - changes.append(port) - - return changes - - def get_updated_ports(self) -> list[dict]: - ports_dwnl = self._get_ports()[1] - changes = [] - - for port in ports_dwnl: - ignore_ports = self.get_added_ports() - if port in ignore_ports: - continue - - path_tmp = f"{self.path}/ports/{port}/port.toml" - path_ins = f"{ccc.PORTS_DIR}/{port}/port.toml" - - cc.dbg_msg(f"checking port '{port}'...") - - data_tmp = toml.load(path_tmp) - data_ins = toml.load(path_ins) - - version_tmp = data_tmp['package']['version'] - version_ins = data_ins['package']['version'] - - if version_tmp != version_ins: - data = { - 'name': port, - 'version': { - 'old': version_ins, - 'new': version_tmp - } - } - changes.append(data) - - return changes - - def get_removed_ports(self) -> list[str]: - ports_inst = self._get_ports()[0] - ports_dwnl = self._get_ports()[1] - changes = [] - - for port in ports_inst: - if port not in ports_dwnl: - changes.append(port) - - return changes - - def get_other_ports(self, file: str = "install") -> list[str]: - ports_inst = self._get_ports()[0] - changes = [] - - for port in ports_inst: - path_ins = f"{ccc.PORTS_DIR}/{port}" - path_tmp = f"{self.path}/ports/{port}" - - if not os.path.exists(path_tmp): - continue - - with open(f"{path_ins}/{file}") as f: - inst_ins = f.read() - - with open(f"{path_tmp}/{file}") as f: - inst_tmp = f.read() - - if inst_ins != inst_tmp: - changes.append(port) - - return changes - - -class Update(Check): - - def __init__(self, path: str): - super().__init__(path) - self.path = path - self.archive = f"{ccc.CACHE_DOWNLOADED_DIR}/ports.txz" - self.url = f"{URL}/-/raw/{BRANCH}/ports.txz" - - def get(self) -> bool: - if os.path.isfile(self.archive): - os.remove(self.archive) - - wget.download(self.url, self.archive) - print() - - return os.path.exists(self.archive) - - def unpack(self) -> bool: - with tarfile.open(self.archive, 'r') as f: - f.extractall(ccc.CACHE_UNPACKED_DIR) - - return os.path.isfile( - f"{ccc.CACHE_UNPACKED_DIR}/ports" - ) - - def install(self) -> bool: - if os.path.isdir( - ccc.PORTS_DIR - ) and os.path.isdir(self.path): - shutil.rmtree(ccc.PORTS_DIR) - else: - raise FileNotFoundError - - shutil.copytree(self.path, ccc.PORTS_DIR) - - return os.path.isdir(ccc.PORTS_DIR) diff --git a/src/libcport/upgrade.py b/src/libcport/upgrade.py deleted file mode 100644 index 8cda26a6be3637839b584de778eb8e37fc5d9d46..0000000000000000000000000000000000000000 --- a/src/libcport/upgrade.py +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/python3 -# -# upgrade.py - module for upgrading the ports -# Copyright (C) 2021, 2022 Michail Krasnov -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import libcport.core as cc -import libcport.install as ci -import libcport.remove as cr -import libcport.datatypes as cd - -import toml - - -def check_exist(port: cd.port) -> bool: - """Checks if the port exists in the database""" - db = cc.CDatabase(port) - in_db: bool = db.check() - db.close() - - return in_db - - -def compare_version(port: cd.port) -> bool: - """Checks for a match between the port versions - specified in the database and those specified - in the ports system""" - - db = cc.CDatabase(port) - - info_db: list = db.get_one() - info_toml: dict = toml.load(f"{port.path}/port.toml") - - ver_in_db: str = info_db[1] - ver_in_toml: str = info_toml['package']['version'] - - return ver_in_db == ver_in_toml - - -def upgrade_soft(port: cd.port) -> int: - db = cc.CDatabase(port) - if not check_exist(port): - cc.Msg.warn(f"Port '{port.name}' isn't in database!") - return 1 - - if compare_version(port): - cc.Msg.err(f"Nothing to do.") - return 1 - - db.remove() - inst = ci.CInstall(port) - - inst.download() - - if not inst.cksum(): - return 1 - - inst.unpack() - r = inst.install() - - if r == 0: - db.add() - - db.close() - - return r - - -def upgrade_hard(port: cd.port) -> int: - db = cc.CDatabase(port) - if not check_exist(port): - cc.Msg.warn(f"Port '{port.name}' isn't in database!") - return 1 - - if compare_version(port): - cc.Msg.err(f"Nothing to do.") - return 1 - - remove = cr.CRemove(port) - remove.remove() - - db.remove() - - inst = ci.CInstall(port) - - inst.download() - - if not inst.cksum(): - return 1 - - inst.unpack() - r = inst.install() - - if r == 0: - db.add() - - db.close() - - return r