49 Commits

Author SHA1 Message Date
8dea5e1e7f Улучшена логика создания конфига при новом запуске и при появлении новых опций (миграция)
Some checks failed
Pre-commit / pre-commit (push) Successful in 6m38s
Create Release / changelog (push) Failing after 2m59s
2025-09-11 23:29:24 +03:00
86a982478e Исправление PrepareDirs вызывался только если пакет действительно нужно собирать
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m43s
Create Release / changelog (push) Successful in 3m8s
2025-09-08 22:31:43 +03:00
8bc82cb95c Добавление статистики
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m56s
Create Release / changelog (push) Successful in 3m27s
Исправление работы с мультипакетами
2025-09-01 01:32:43 +03:00
9783ce37de Добавление возможности обновления системным пакетным менеджером при alr up
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m39s
2025-08-28 12:03:14 +03:00
b852688ab0 Исправление фильтрации имён пакетов в скрипте установки
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m27s
2025-08-27 12:49:03 +03:00
2ff5e6f7b6 изменение способа определения имён для добавления в релиз №2
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m29s
Create Release / changelog (push) Successful in 3m17s
2025-08-27 12:33:05 +03:00
c9639b7073 изменение способа определения имён для добавления в релиз
Some checks failed
Pre-commit / pre-commit (push) Has been cancelled
Create Release / changelog (push) Successful in 3m6s
2025-08-27 12:22:22 +03:00
c1847e1191 изменение способа определения имён для добавления в релиз
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m41s
Create Release / changelog (push) Successful in 3m2s
2025-08-27 12:10:42 +03:00
f2b0f57c12 1. internal/manager/common.go - Модифицировали getCmd для
Some checks failed
Pre-commit / pre-commit (push) Successful in 5m32s
Create Release / changelog (push) Has been cancelled
проверки root/CI перед использованием sudo
  2. internal/utils/utils.go - Функция
  EnsureTempDirWithRootOwner теперь не использует группу
  wheel в CI
  3. internal/utils/cmd.go - Функция
  EnuseIsPrivilegedGroupMember пропускает проверку wheel в
  CI
  4. fix.go - Добавили функцию execWithPrivileges для
  условного использования sudo
  5. scripts/install.sh - Добавили проверку root перед
  использованием sudo
2025-08-27 11:46:18 +03:00
59cc41e94c Внесение логики для запуска из под root
Some checks failed
Pre-commit / pre-commit (push) Successful in 5m40s
Create Release / changelog (push) Failing after 2m27s
2025-08-27 01:45:54 +03:00
75ece6dfcc Исправление для авторелизов
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m24s
2025-08-27 01:06:58 +03:00
6af712f1d5 исправление pre-commit hooks для корректной работы с изменёнными файлами
Some checks failed
Pre-commit / pre-commit (push) Successful in 5m31s
Create Release / changelog (push) Failing after 2m29s
2025-08-27 00:49:16 +03:00
bad225c6b1 - Добавлено автоматическое определение архитектуры системы
Some checks failed
Pre-commit / pre-commit (push) Failing after 4m59s
- Использование API Gitea вместо парсинга HTML
- Добавлен fallback на парсинг HTML если API недоступен
- Улучшена обработка ошибок при загрузке
- Добавлена проверка целостности загруженного файла
- Использование trap для гарантированной очистки временных файлов
- Исправлена логика выбора файлов для разных архитектур
- Добавлен вывод размера загруженного пакета"
2025-08-27 00:36:58 +03:00
4b3bf44aaa fix: улучшение pre-commit hooks для правильной обработки изменений файлов
Some checks failed
Pre-commit / pre-commit (push) Failing after 4m51s
- Создан fmt-precommit.sh для корректной обработки форматирования
- Создан test-coverage-precommit.sh для обработки изменений покрытия
- Скрипты всегда возвращают 0 при успешном выполнении
- Автоматически добавляют изменённые файлы в staging area
2025-08-27 00:14:24 +03:00
67b3c40430 исправление README.md
Some checks failed
Pre-commit / pre-commit (push) Failing after 5m5s
2025-08-27 00:06:24 +03:00
4948e6b8fc исправление fmt
Some checks failed
Pre-commit / pre-commit (push) Failing after 5m9s
2025-08-26 23:49:36 +03:00
292125a8ff исправление теста dlcache_test.go №2
Some checks failed
Pre-commit / pre-commit (push) Failing after 4m59s
2025-08-26 23:41:33 +03:00
77055aa2cb исправление теста dlcache_test.go
Some checks failed
Pre-commit / pre-commit (push) Failing after 5m11s
2025-08-26 23:16:31 +03:00
737bf68f95 исправление i18n-precommit
Some checks failed
Pre-commit / pre-commit (push) Failing after 5m14s
2025-08-26 22:43:39 +03:00
1089e8a3f3 Исправление работоспособности pre-commit.yaml 2
Some checks failed
Pre-commit / pre-commit (push) Failing after 5m42s
2025-08-26 22:25:03 +03:00
aa42ab0607 Исправление работоспособности pre-commit.yaml
Some checks failed
Pre-commit / pre-commit (push) Failing after 6m27s
2025-08-26 22:14:01 +03:00
51fa7ca6fb убрана лишняя зависимость bindfs и избыточное использование дополнительного пользователя alr 2025-08-26 22:09:28 +03:00
ab41700004 первичная итерация генератора из aur пакетов 2025-08-21 18:47:37 +03:00
7cb1bc9548 первичная итерация генератора из aur пакетов
Some checks failed
Update alr-git / changelog (push) Failing after 25s
2025-08-21 18:44:43 +03:00
07187da423 - Изменение ссылки на wiki в README.md
All checks were successful
Update alr-git / changelog (push) Successful in 26s
2025-07-27 23:50:45 +03:00
802fe2b0b2 tag 0.0.26
All checks were successful
Update alr-git / changelog (push) Successful in 24s
2025-07-11 14:53:52 +03:00
aa08c04e0c fix: use single output format for alt list and alr list -I
Some checks failed
Pre-commit / pre-commit (pull_request) Successful in 5m25s
Update alr-git / changelog (push) Successful in 22s
Create Release / changelog (push) Has been cancelled
2025-07-09 20:38:24 +03:00
f42be105ad feat: add import info from alr-repo.toml
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m5s
Update alr-git / changelog (push) Successful in 24s
2025-07-07 17:45:20 +03:00
1cc408ad7d refactor: generate plugin executors 2025-07-07 13:56:09 +03:00
4899e203bb feat: allow finding packages by "{repo}/{pkg}" and "{pkg}+alr-{repo}"
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m32s
Update alr-git / changelog (push) Successful in 23s
2025-07-06 16:47:46 +03:00
67a6cb31de refactor: migrate e2e tests from efficientgo/e2e to capytest
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m55s
Update alr-git / changelog (push) Successful in 24s
2025-07-05 20:50:20 +03:00
5e24940ef8 fix: firejail integration
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m39s
Update alr-git / changelog (push) Successful in 32s
use buildContents for correct handling symlinks
2025-07-03 17:29:53 +03:00
a600feb083 security: update vulnerable packages
Some checks failed
Pre-commit / pre-commit (pull_request) Successful in 5m7s
Update alr-git / changelog (push) Successful in 23s
Create Release / changelog (push) Failing after 17s
Vulnerabilities detected by Trivy scan:
- github.com/go-viper/mapstructure/v2 (GHSA-fv92-fjc5-jj9h)
2025-06-30 08:50:36 +03:00
7060e4f551 chore: refactor Makefile with build and install improvements
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m29s
Update alr-git / changelog (push) Successful in 23s
- fix typo in INSTALLED_BIN variable name
- add GENERATE flag to optionally skip go generate
- add CREATE_SYSTEM_RESOURCES flag for user/dir creation control
- make GIT_VERSION optional with ?= operator
- add informative messages for skipped operations
2025-06-30 08:27:14 +03:00
d77ca4c384 feat: config command
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m5s
Update alr-git / changelog (push) Successful in 27s
2025-06-29 21:26:00 +03:00
6355f25089 feat: add ability to remove build_deps
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m36s
Update alr-git / changelog (push) Successful in 26s
2025-06-28 20:19:07 +03:00
a83561b6a5 fix: implement dirlfs to ignore symlinks
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m16s
Update alr-git / changelog (push) Successful in 25s
2025-06-28 01:01:06 +03:00
4b06809a39 fix: quote files-find output and fail on pattern not exists (#123)
All checks were successful
Update alr-git / changelog (push) Successful in 24s
closes #122
closes #121

Reviewed-on: #123
Co-authored-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
Co-committed-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
2025-06-27 20:22:10 +00:00
401c41160c chore: pass all options to download
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m26s
Update alr-git / changelog (push) Successful in 24s
2025-06-25 19:52:54 +03:00
5e1eeabd04 chore: simplify dlcache initialization
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m31s
Update alr-git / changelog (push) Successful in 23s
2025-06-25 19:18:11 +03:00
db19133254 fix: correct handling opts.PostprocDisabled
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m35s
Update alr-git / changelog (push) Successful in 29s
2025-06-25 08:07:58 +03:00
e8202060d8 chore: remove debug slog.Warn
Some checks failed
Pre-commit / pre-commit (pull_request) Successful in 6m0s
Update alr-git / changelog (push) Successful in 26s
Create Release / changelog (push) Failing after 19s
2025-06-22 17:27:57 +03:00
c4a92c67d4 fix parsing overrides
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m34s
Update alr-git / changelog (push) Successful in 29s
2025-06-22 12:44:21 +03:00
85878f69d3 feat: add checksum for torrent downloader
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m0s
Update alr-git / changelog (push) Successful in 28s
2025-06-20 20:12:43 +03:00
6bccce1db4 feat: add checksum for git downloader 2025-06-20 19:35:22 +03:00
b5474b1eb4 ci: disable building alr-bin
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 7m5s
Update alr-git / changelog (push) Successful in 30s
2025-06-20 09:21:03 +03:00
51fdea781b fix: correct pull for multiple repos 2025-06-20 09:08:34 +03:00
4c1f2ea90f feat: support mirrors
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m45s
Update alr-git / changelog (push) Successful in 25s
2025-06-19 19:00:08 +03:00
7fa7f8ba82 security: update vulnerable packages
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m11s
Update alr-git / changelog (push) Successful in 29s
Vulnerabilities detected by Trivy scan:
- github.com/cloudflare/circl (GHSA-2x5j-vhc8-9cwm)
2025-06-19 12:10:31 +03:00
112 changed files with 6138 additions and 2070 deletions

View File

@@ -19,7 +19,7 @@ name: Pre-commit
on: on:
push: push:
branches: [ main ] branches: [ master ]
pull_request: pull_request:

View File

@@ -47,7 +47,7 @@ jobs:
- name: Prepare for install - name: Prepare for install
run: | run: |
apt-get update && apt-get install -y libcap2-bin bindfs apt-get update
- name: Build alr - name: Build alr
env: env:
@@ -85,12 +85,17 @@ jobs:
sed -i "s/release='[0-9]\+'/release='1'/g" alr-default/alr-bin/alr.sh sed -i "s/release='[0-9]\+'/release='1'/g" alr-default/alr-bin/alr.sh
- name: Install alr - name: Install alr
env:
CREATE_SYSTEM_RESOURCES: 0
run: | run: |
make install make install
# temporary fix - name: Prepare directories for ALR
groupadd wheel run: |
usermod -aG wheel root # Создаём необходимые директории для работы alr build
mkdir -p /tmp/alr/dl /tmp/alr/pkgs /var/cache/alr
chmod -R 777 /tmp/alr
chmod -R 755 /var/cache/alr
- name: Build packages - name: Build packages
run: | run: |
@@ -105,16 +110,6 @@ jobs:
with: with:
body: ${{ steps.changes.outputs.changes }} body: ${{ steps.changes.outputs.changes }}
files: |- files: |-
alr-bin+alr-default_${{ env.VERSION }}-1.red80_amd64.deb \ alr-bin*.deb
alr-bin+alr-default-${{ env.VERSION }}-1-x86_64.pkg.tar.zst \ alr-bin*.rpm
alr-bin+alr-default-${{ env.VERSION }}-1.red80.x86_64.rpm \ alr-bin*.pkg.tar.zst
alr-bin+alr-default-${{ env.VERSION }}-alt1.x86_64.rpm
- name: Commit changes
run: |
cd alr-default
git config user.name "gitea"
git config user.email "admin@plemya-x.ru"
git add .
git commit -m "Обновление версии до ${{ env.VERSION }}"
git push

View File

@@ -1,69 +0,0 @@
# ALR - Any Linux Repository
# Copyright (C) 2025 The ALR Authors
#
# 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 <http://www.gnu.org/licenses/>.
name: Update alr-git
on:
push:
branches:
- master
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v6
- name: Setup alr-spec
run: |
uv tool install alr-spec==0.0.5
- name: Install alr
run: |
apt-get update && apt-get install -y libcap2-bin
curl -fsS https://gitea.plemya-x.ru/Plemya-x/ALR/raw/branch/master/scripts/install.sh | bash
- name: Checkout this repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set ALR version
run: |
echo "NEW_ALR_VERSION=$(alr helper git-version)" >> $GITHUB_ENV
- name: Checkout alr-default repository
uses: actions/checkout@v4
with:
repository: Plemya-x/alr-default
token: ${{ secrets.GITEAPUBLIC }}
path: alr-default
- name: Update version
working-directory: ./alr-default/alr-git
run: |
alr-spec set-field version $NEW_ALR_VERSION
alr-spec set-field release 1
- name: Commit changes
run: |
cd alr-default
git config user.name "gitea"
git config user.email "admin@plemya-x.ru"
git add .
git commit -m "Обновление версии до $NEW_ALR_VERSION"
git push

7
.gitignore vendored
View File

@@ -3,11 +3,12 @@
/cmd/alr-api-server/alr-api-server /cmd/alr-api-server/alr-api-server
/dist/ /dist/
/internal/config/version.txt /internal/config/version.txt
.fleet .fleet/
.idea .idea/
.gigaide .gigaide/
*.out *.out
e2e-tests/alr e2e-tests/alr
CLAUDE.md
commit_msg.txt commit_msg.txt

View File

@@ -36,11 +36,14 @@ linters:
- unused - unused
- errcheck - errcheck
- typecheck - typecheck
# - forbidigo - wrapcheck
issues: issues:
fix: true fix: true
exclude-rules: exclude-rules:
- linters:
- wrapcheck
path-except: "internal/repos/find.go"
- path: _test\.go - path: _test\.go
linters: linters:
- errcheck - errcheck

View File

@@ -19,13 +19,13 @@ repos:
hooks: hooks:
- id: test-coverage - id: test-coverage
name: Run test coverage name: Run test coverage
entry: make test-coverage entry: bash scripts/test-coverage-precommit.sh
language: system language: system
pass_filenames: false pass_filenames: false
- id: fmt - id: fmt
name: Format code name: Format code
entry: make fmt entry: bash scripts/fmt-precommit.sh
language: system language: system
pass_filenames: false pass_filenames: false
@@ -37,6 +37,7 @@ repos:
- id: i18n - id: i18n
name: Update i18n name: Update i18n
entry: make i18n entry: bash scripts/i18n-precommit.sh
language: system language: system
pass_filenames: false pass_filenames: false
always_run: true

View File

@@ -1,16 +1,21 @@
NAME := alr NAME := alr
GIT_VERSION = $(shell git describe --tags ) GIT_VERSION ?= $(shell git describe --tags )
IGNORE_ROOT_CHECK ?= 0 IGNORE_ROOT_CHECK ?= 0
DESTDIR ?= DESTDIR ?=
PREFIX ?= /usr/local PREFIX ?= /usr/local
BIN := ./$(NAME) BIN := ./$(NAME)
INSTALED_BIN := $(DESTDIR)/$(PREFIX)/bin/$(NAME) INSTALLED_BIN := $(DESTDIR)/$(PREFIX)/bin/$(NAME)
COMPLETIONS_DIR := ./scripts/completion COMPLETIONS_DIR := ./scripts/completion
BASH_COMPLETION := $(COMPLETIONS_DIR)/bash BASH_COMPLETION := $(COMPLETIONS_DIR)/bash
ZSH_COMPLETION := $(COMPLETIONS_DIR)/zsh ZSH_COMPLETION := $(COMPLETIONS_DIR)/zsh
INSTALLED_BASH_COMPLETION := $(DESTDIR)$(PREFIX)/share/bash-completion/completions/$(NAME) INSTALLED_BASH_COMPLETION := $(DESTDIR)$(PREFIX)/share/bash-completion/completions/$(NAME)
INSTALLED_ZSH_COMPLETION := $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_$(NAME) INSTALLED_ZSH_COMPLETION := $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_$(NAME)
GENERATE ?= 1
CREATE_SYSTEM_RESOURCES ?= 1
ROOT_DIRS := /var/cache/alr /etc/alr
ADD_LICENSE_BIN := go run github.com/google/addlicense@4caba19b7ed7818bb86bc4cd20411a246aa4a524 ADD_LICENSE_BIN := go run github.com/google/addlicense@4caba19b7ed7818bb86bc4cd20411a246aa4a524
GOLANGCI_LINT_BIN := go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4 GOLANGCI_LINT_BIN := go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4
XGOTEXT_BIN := go run github.com/Tom5521/xgotext@v1.2.0 XGOTEXT_BIN := go run github.com/Tom5521/xgotext@v1.2.0
@@ -21,6 +26,11 @@ build: check-no-root $(BIN)
export CGO_ENABLED := 0 export CGO_ENABLED := 0
$(BIN): $(BIN):
ifeq ($(GENERATE),1)
go generate ./...
else
@echo "Skipping go generate (GENERATE=0)"
endif
go build -ldflags="-X 'gitea.plemya-x.ru/Plemya-x/ALR/internal/config.Version=$(GIT_VERSION)'" -o $@ go build -ldflags="-X 'gitea.plemya-x.ru/Plemya-x/ALR/internal/config.Version=$(GIT_VERSION)'" -o $@
check-no-root: check-no-root:
@@ -31,20 +41,21 @@ check-no-root:
fi fi
install: \ install: \
$(INSTALED_BIN) \ $(INSTALLED_BIN) \
$(INSTALLED_BASH_COMPLETION) \ $(INSTALLED_BASH_COMPLETION) \
$(INSTALLED_ZSH_COMPLETION) $(INSTALLED_ZSH_COMPLETION)
@echo "Installation done!" @echo "Installation done!"
$(INSTALED_BIN): $(BIN) $(INSTALLED_BIN): $(BIN)
install -Dm755 $< $@ install -Dm755 $< $@
setcap cap_setuid,cap_setgid+ep $(INSTALED_BIN) ifeq ($(CREATE_SYSTEM_RESOURCES),1)
@if id alr >/dev/null 2>&1; then \ @for dir in $(ROOT_DIRS); do \
echo "User 'alr' already exists. Skipping."; \ install -d -m 775 $$dir; \
else \ chgrp wheel $$dir; \
useradd -r -s /usr/sbin/nologin alr; \ done
fi else
install -d -o alr -g alr -m 755 /var/cache/alr /etc/alr @echo "Skipping root dir creation (CREATE_SYSTEM_RESOURCES=0)"
endif
$(INSTALLED_BASH_COMPLETION): $(BASH_COMPLETION) $(INSTALLED_BASH_COMPLETION): $(BASH_COMPLETION)
install -Dm755 $< $@ install -Dm755 $< $@
@@ -54,7 +65,7 @@ $(INSTALLED_ZSH_COMPLETION): $(ZSH_COMPLETION)
uninstall: uninstall:
rm -f \ rm -f \
$(INSTALED_BIN) \ $(INSTALLED_BIN) \
$(INSTALLED_BASH_COMPLETION) \ $(INSTALLED_BASH_COMPLETION) \
$(INSTALLED_ZSH_COMPLETION) $(INSTALLED_ZSH_COMPLETION)
@@ -77,7 +88,7 @@ i18n:
bash scripts/i18n-badge.sh bash scripts/i18n-badge.sh
test-coverage: test-coverage:
go test ./... -v -coverpkg=./... -coverprofile=coverage.out go test -tags=test ./... -v -coverpkg=./... -coverprofile=coverage.out
bash scripts/coverage-badge.sh bash scripts/coverage-badge.sh
update-deps-cve: update-deps-cve:

View File

@@ -44,7 +44,7 @@ ALR был создан потому, что упаковка программн
## Документация ## Документация
Документация находится в [Wiki](https://disc.plemya-x.ru/c/alr/wiki-alr). Документация находится в [Wiki](https://alr.plemya-x.ru/wiki/ALR).
--- ---
@@ -52,23 +52,21 @@ ALR был создан потому, что упаковка программн
Репозитории alr - это git-хранилища, которые содержат каталог для каждого пакета с файлом `alr.sh` внутри. Файл `alr.sh` содержит все инструкции по сборке пакета и информацию о нем. Скрипты `alr.sh` аналогичны скриптам Aur PKGBUILD. Репозитории alr - это git-хранилища, которые содержат каталог для каждого пакета с файлом `alr.sh` внутри. Файл `alr.sh` содержит все инструкции по сборке пакета и информацию о нем. Скрипты `alr.sh` аналогичны скриптам Aur PKGBUILD.
Например, репозиторий с ALR [Plemya-x/alr-default](https://gitea.plemya-x.ru/Plemya-x/alr-default.git) Например, репозиторий с ALR [alr-default](https://gitea.plemya-x.ru/Plemya-x/alr-default.git)
``` ```
alr repo add alr-default https://gitea.plemya-x.ru/Plemya-x/alr-default.git alr repo add alr-default https://gitea.plemya-x.ru/Plemya-x/alr-default.git
``` ```
Репозиторий пакетов [Plemya-x/alr-repo](https://gitea.plemya-x.ru/Plemya-x/alr-repo.git) можно подключить так: Репозиторий пакетов [alr-repo](https://gitea.plemya-x.ru/Plemya-x/alr-repo.git) можно подключить так:
``` ```
alr repo add alr-repo https://gitea.plemya-x.ru/Plemya-x/alr-repo.git alr repo add alr-repo https://gitea.plemya-x.ru/Plemya-x/alr-repo.git
``` ```
Репозиторий Linux-Gaming [Plemya-x/alr-LG](https://gitea.plemya-x.ru/Plemya-x/alr-LG.git) можно подключить так: Репозиторий Linux-Gaming [alr-LG](https://gitea.plemya-x.ru/Plemya-x/alr-LG.git) можно подключить так:
``` ```
alr repo add alr-LG https://gitea.plemya-x.ru/Plemya-x/alr-LG.git alr repo add alr-LG https://git.linux-gaming.ru/Linux-Gaming/alr-LG.git
``` ```
--- ---
## Соцсети ## Соцсети
VK - https://vk.com/plemya_kh
Telegram - https://t.me/plemyakh Telegram - https://t.me/plemyakh
## Спасибы ## Спасибы

View File

@@ -11,7 +11,7 @@
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"> <g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="33.5" y="15" fill="#010101" fill-opacity=".3">coverage</text> <text x="33.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
<text x="33.5" y="14">coverage</text> <text x="33.5" y="14">coverage</text>
<text x="86" y="15" fill="#010101" fill-opacity=".3">19.4%</text> <text x="86" y="15" fill="#010101" fill-opacity=".3">18.9%</text>
<text x="86" y="14">19.4%</text> <text x="86" y="14">18.9%</text>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 926 B

After

Width:  |  Height:  |  Size: 926 B

View File

@@ -23,7 +23,6 @@ import (
"log/slog" "log/slog"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@@ -73,12 +72,6 @@ func BuildCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Error getting working directory"), err) return cliutils.FormatCliExit(gotext.Get("Error getting working directory"), err)
} }
wd, wdCleanup, err := Mount(wd)
if err != nil {
return err
}
defer wdCleanup()
ctx := c.Context ctx := c.Context
deps, err := appbuilder. deps, err := appbuilder.
@@ -133,15 +126,7 @@ func BuildCmd() *cli.Command {
// TODO: handle multiple packages // TODO: handle multiple packages
packageInput := c.String("package") packageInput := c.String("package")
arr := strings.Split(packageInput, "/") pkgs, _, err := deps.Repos.FindPkgs(ctx, []string{packageInput})
var packageSearch string
if len(arr) == 2 {
packageSearch = arr[1]
} else {
packageSearch = arr[0]
}
pkgs, _, err := deps.Repos.FindPkgs(ctx, []string{packageSearch})
if err != nil { if err != nil {
return cliutils.FormatCliExit("failed to find pkgs", err) return cliutils.FormatCliExit("failed to find pkgs", err)
} }
@@ -165,19 +150,9 @@ func BuildCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Nothing to build"), nil) return cliutils.FormatCliExit(gotext.Get("Nothing to build"), nil)
} }
if scriptArgs != nil {
scriptFile := filepath.Base(scriptArgs.Script)
newScriptDir, scriptDirCleanup, err := Mount(filepath.Dir(scriptArgs.Script))
if err != nil {
return err
}
defer scriptDirCleanup()
scriptArgs.Script = filepath.Join(newScriptDir, scriptFile)
}
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
installer, installerClose, err := build.GetSafeInstaller() installer, installerClose, err := build.GetSafeInstaller()
if err != nil { if err != nil {
@@ -185,9 +160,7 @@ func BuildCmd() *cli.Command {
} }
defer installerClose() defer installerClose()
if err := utils.ExitIfCantSetNoNewPrivs(); err != nil {
return err
}
scripter, scripterClose, err := build.GetSafeScriptExecutor() scripter, scripterClose, err := build.GetSafeScriptExecutor()
if err != nil { if err != nil {

236
config.go Normal file
View File

@@ -0,0 +1,236 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package main
import (
"fmt"
"strconv"
"strings"
"github.com/goccy/go-yaml"
"github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
)
func ConfigCmd() *cli.Command {
return &cli.Command{
Name: "config",
Usage: gotext.Get("Manage config"),
Subcommands: []*cli.Command{
ShowCmd(),
SetConfig(),
GetConfig(),
},
}
}
func ShowCmd() *cli.Command {
return &cli.Command{
Name: "show",
Usage: gotext.Get("Show config"),
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
return nil
}),
Action: func(c *cli.Context) error {
deps, err := appbuilder.
New(c.Context).
WithConfig().
Build()
if err != nil {
return err
}
defer deps.Defer()
content, err := deps.Cfg.ToYAML()
if err != nil {
return err
}
fmt.Println(content)
return nil
},
}
}
var configKeys = []string{
"rootCmd",
"useRootCmd",
"pagerStyle",
"autoPull",
"logLevel",
"ignorePkgUpdates",
"updateSystemOnUpgrade",
}
func SetConfig() *cli.Command {
return &cli.Command{
Name: "set",
Usage: gotext.Get("Set config value"),
ArgsUsage: gotext.Get("<key> <value>"),
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
if c.Args().Len() == 0 {
for _, key := range configKeys {
fmt.Println(key)
}
return nil
}
return nil
}),
Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 2 {
return cliutils.FormatCliExit("missing args", nil)
}
key := c.Args().Get(0)
value := c.Args().Get(1)
deps, err := appbuilder.
New(c.Context).
WithConfig().
Build()
if err != nil {
return err
}
defer deps.Defer()
switch key {
case "rootCmd":
deps.Cfg.System.SetRootCmd(value)
case "useRootCmd":
boolValue, err := strconv.ParseBool(value)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("invalid boolean value for %s: %s", key, value), err)
}
deps.Cfg.System.SetUseRootCmd(boolValue)
case "pagerStyle":
deps.Cfg.System.SetPagerStyle(value)
case "autoPull":
boolValue, err := strconv.ParseBool(value)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("invalid boolean value for %s: %s", key, value), err)
}
deps.Cfg.System.SetAutoPull(boolValue)
case "logLevel":
deps.Cfg.System.SetLogLevel(value)
case "ignorePkgUpdates":
var updates []string
if value != "" {
updates = strings.Split(value, ",")
for i, update := range updates {
updates[i] = strings.TrimSpace(update)
}
}
deps.Cfg.System.SetIgnorePkgUpdates(updates)
case "updateSystemOnUpgrade":
boolValue, err := strconv.ParseBool(value)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("invalid boolean value for %s: %s", key, value), err)
}
deps.Cfg.System.SetUpdateSystemOnUpgrade(boolValue)
case "repo", "repos":
return cliutils.FormatCliExit(gotext.Get("use 'repo add/remove' commands to manage repositories"), nil)
default:
return cliutils.FormatCliExit(gotext.Get("unknown config key: %s", key), nil)
}
if err := deps.Cfg.System.Save(); err != nil {
return cliutils.FormatCliExit(gotext.Get("failed to save config"), err)
}
fmt.Println(gotext.Get("Successfully set %s = %s", key, value))
return nil
}),
}
}
func GetConfig() *cli.Command {
return &cli.Command{
Name: "get",
Usage: gotext.Get("Get config value"),
ArgsUsage: gotext.Get("<key>"),
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
if c.Args().Len() == 0 {
for _, key := range configKeys {
fmt.Println(key)
}
return nil
}
return nil
}),
Action: func(c *cli.Context) error {
deps, err := appbuilder.
New(c.Context).
WithConfig().
Build()
if err != nil {
return err
}
defer deps.Defer()
if c.Args().Len() == 0 {
content, err := deps.Cfg.ToYAML()
if err != nil {
return cliutils.FormatCliExit("failed to serialize config", err)
}
fmt.Print(content)
return nil
}
key := c.Args().Get(0)
switch key {
case "rootCmd":
fmt.Println(deps.Cfg.RootCmd())
case "useRootCmd":
fmt.Println(deps.Cfg.UseRootCmd())
case "pagerStyle":
fmt.Println(deps.Cfg.PagerStyle())
case "autoPull":
fmt.Println(deps.Cfg.AutoPull())
case "logLevel":
fmt.Println(deps.Cfg.LogLevel())
case "ignorePkgUpdates":
updates := deps.Cfg.IgnorePkgUpdates()
if len(updates) == 0 {
fmt.Println("[]")
} else {
fmt.Println(strings.Join(updates, ", "))
}
case "updateSystemOnUpgrade":
fmt.Println(deps.Cfg.UpdateSystemOnUpgrade())
case "repo", "repos":
repos := deps.Cfg.Repos()
if len(repos) == 0 {
fmt.Println("[]")
} else {
repoData, err := yaml.Marshal(repos)
if err != nil {
return cliutils.FormatCliExit("failed to serialize repos", err)
}
fmt.Print(string(repoData))
}
default:
return cliutils.FormatCliExit(gotext.Get("unknown config key: %s", key), nil)
}
return nil
},
}
}

View File

@@ -0,0 +1,5 @@
- name: alr-repo
url: https://gitea.plemya-x.ru/Plemya-x/repo-for-tests
ref: main
mirrors:
- https://github.com/example/example.git

View File

@@ -0,0 +1 @@
alr-repo/foo-pkg 1.0.0-1

View File

@@ -0,0 +1,2 @@
alr-repo/bar-pkg 1.0.0-1
alr-repo/foo-pkg 1.0.0-1

View File

@@ -19,54 +19,24 @@
package e2etests_test package e2etests_test
import ( import (
"bytes"
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
"github.com/stretchr/testify/assert"
) )
func TestE2EAlrAddRepo(t *testing.T) { func TestE2EAlrAddRepo(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"add-repo-remove-repo", "add-repo-remove-repo",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
err := r.Exec(e2e.NewCommand( execShouldNoError(t, r, "sudo", "alr", "addrepo", "--name", "alr-repo", "--url", "https://gitea.plemya-x.ru/Plemya-x/alr-repo.git")
"sudo", execShouldNoError(t, r, "bash", "-c", "cat /etc/alr/alr.toml")
"alr", execShouldNoError(t, r, "sudo", "alr", "removerepo", "--name", "alr-repo")
"addrepo",
"--name",
"alr-repo",
"--url",
"https://gitea.plemya-x.ru/Plemya-x/alr-repo.git",
))
assert.NoError(t, err)
err = r.Exec(e2e.NewCommand( r.Command("bash", "-c", "cat /etc/alr/alr.toml").
"bash", ExpectStdoutContains("repo = []").
"-c", Run(t)
"cat /etc/alr/alr.toml",
))
assert.NoError(t, err)
err = r.Exec(e2e.NewCommand(
"sudo",
"alr",
"removerepo",
"--name",
"alr-repo",
))
assert.NoError(t, err)
var buf bytes.Buffer
err = r.Exec(e2e.NewCommand(
"bash",
"-c",
"cat /etc/alr/alr.toml",
), e2e.WithExecOptionStdout(&buf))
assert.NoError(t, err)
assert.Contains(t, buf.String(), "rootCmd")
}, },
) )
} }

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EBashCompletion(t *testing.T) { func TestE2EBashCompletion(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"bash-completion", "bash-completion",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
execShouldNoError(t, r, "alr", "install", "--generate-bash-completion") execShouldNoError(t, r, "alr", "install", "--generate-bash-completion")
}, },
) )

View File

@@ -19,84 +19,13 @@
package e2etests_test package e2etests_test
import ( import (
"crypto/sha256"
"encoding/hex"
"fmt" "fmt"
"io"
"log"
"os"
"testing" "testing"
"time"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
"github.com/stretchr/testify/assert" "go.alt-gnome.ru/capytest/providers/podman"
expect "github.com/tailscale/goexpect"
) )
// DebugWriter оборачивает io.Writer и логирует все записываемые данные.
type DebugWriter struct {
prefix string
writer io.Writer
}
func (d *DebugWriter) Write(p []byte) (n int, err error) {
log.Printf("%s: Writing data: %q", d.prefix, p) // Логируем данные
return d.writer.Write(p)
}
// DebugReader оборачивает io.Reader и логирует все читаемые данные.
type DebugReader struct {
prefix string
reader io.Reader
}
func (d *DebugReader) Read(p []byte) (n int, err error) {
n, err = d.reader.Read(p)
if n > 0 {
log.Printf("%s: Read data: %q", d.prefix, p[:n]) // Логируем данные
}
return n, err
}
func e2eSpawn(runnable e2e.Runnable, command e2e.Command, timeout time.Duration, opts ...expect.Option) (expect.Expecter, <-chan error, error, *io.PipeWriter) {
resCh := make(chan error)
// Создаем pipe для stdin и stdout
stdinReader, stdinWriter := io.Pipe()
stdoutReader, stdoutWriter := io.Pipe()
debugStdinReader := &DebugReader{prefix: "STDIN", reader: stdinReader}
debugStdoutWriter := &DebugWriter{prefix: "STDOUT", writer: stdoutWriter}
go func() {
err := runnable.Exec(
command,
e2e.WithExecOptionStdout(debugStdoutWriter),
e2e.WithExecOptionStdin(debugStdinReader),
e2e.WithExecOptionStderr(debugStdoutWriter),
)
resCh <- err
}()
exp, chnErr, err := expect.SpawnGeneric(&expect.GenOptions{
In: stdinWriter,
Out: stdoutReader,
Wait: func() error {
return <-resCh
},
Close: func() error {
stdinWriter.Close()
stdoutReader.Close()
return nil
},
Check: func() bool { return true },
}, timeout, expect.Verbose(true), expect.VerboseWriter(os.Stdout))
return exp, chnErr, err, stdinWriter
}
var ALL_SYSTEMS []string = []string{ var ALL_SYSTEMS []string = []string{
"ubuntu-24.04", "ubuntu-24.04",
"alt-sisyphus", "alt-sisyphus",
@@ -120,71 +49,20 @@ var COMMON_SYSTEMS []string = []string{
"ubuntu-24.04", "ubuntu-24.04",
} }
func dockerMultipleRun(t *testing.T, name string, ids []string, f func(t *testing.T, runnable e2e.Runnable)) { func execShouldNoError(t *testing.T, r capytest.Runner, cmd string, args ...string) {
t.Run(name, func(t *testing.T) { t.Helper()
for _, id := range ids { r.Command(cmd, args...).ExpectSuccess().Run(t)
t.Run(id, func(t *testing.T) {
t.Parallel()
dockerName := fmt.Sprintf("alr-test-%s-%s", name, id)
hash := sha256.New()
hash.Write([]byte(dockerName))
hashSum := hash.Sum(nil)
hashString := hex.EncodeToString(hashSum)
truncatedHash := hashString[:8]
e, err := e2e.New(e2e.WithVerbose(), e2e.WithName(fmt.Sprintf("alr-%s", truncatedHash)))
assert.NoError(t, err)
t.Cleanup(e.Close)
imageId := fmt.Sprintf("ghcr.io/maks1ms/alr-e2e-test-image-%s", id)
runnable := e.Runnable(dockerName).Init(
e2e.StartOptions{
Image: imageId,
Volumes: []string{
"./alr:/tmp/alr",
},
Privileged: true,
},
)
assert.NoError(t, e2e.StartAndWaitReady(runnable))
err = runnable.Exec(e2e.NewCommand("/bin/alr-test-setup", "alr-install"))
if err != nil {
panic(err)
}
err = runnable.Exec(e2e.NewCommand("/bin/alr-test-setup", "passwordless-sudo-setup"))
if err != nil {
panic(err)
}
f(t, runnable)
})
}
})
} }
func execShouldNoError(t *testing.T, r e2e.Runnable, cmd string, args ...string) { func execShouldError(t *testing.T, r capytest.Runner, cmd string, args ...string) {
assert.NoError(t, r.Exec(e2e.NewCommand(cmd, args...))) t.Helper()
} r.Command(cmd, args...).ExpectFailure().Run(t)
func execShouldError(t *testing.T, r e2e.Runnable, cmd string, args ...string) {
assert.Error(t, r.Exec(e2e.NewCommand(cmd, args...)))
}
func runTestCommands(t *testing.T, r e2e.Runnable, timeout time.Duration, expects []expect.Batcher) {
exp, _, err, _ := e2eSpawn(
r,
e2e.NewCommand("/bin/bash"), 25*time.Second,
expect.Verbose(true),
)
assert.NoError(t, err)
_, err = exp.ExpectBatch(
expects,
timeout,
)
assert.NoError(t, err)
} }
const REPO_NAME_FOR_E2E_TESTS = "alr-repo" const REPO_NAME_FOR_E2E_TESTS = "alr-repo"
const REPO_URL_FOR_E2E_TESTS = "https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git" const REPO_URL_FOR_E2E_TESTS = "https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git"
func defaultPrepare(t *testing.T, r e2e.Runnable) { func defaultPrepare(t *testing.T, r capytest.Runner) {
execShouldNoError(t, r, execShouldNoError(t, r,
"sudo", "sudo",
"alr", "alr",
@@ -200,3 +78,19 @@ func defaultPrepare(t *testing.T, r e2e.Runnable) {
"ref", "ref",
) )
} }
func runMatrixSuite(t *testing.T, name string, images []string, test func(t *testing.T, r capytest.Runner)) {
t.Helper()
for _, image := range images {
ts := capytest.NewTestSuite(t, podman.Provider(
podman.WithImage(fmt.Sprintf("ghcr.io/maks1ms/alr-e2e-test-image-%s", image)),
podman.WithVolumes("./alr:/tmp/alr"),
podman.WithPrivileged(true),
))
ts.BeforeEach(func(t *testing.T, r capytest.Runner) {
execShouldNoError(t, r, "/bin/alr-test-setup", "alr-install")
execShouldNoError(t, r, "/bin/alr-test-setup", "passwordless-sudo-setup")
})
ts.Run(fmt.Sprintf("%s/%s", name, image), test)
}
}

View File

@@ -22,15 +22,15 @@ import (
"fmt" "fmt"
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EFirejailedPackage(t *testing.T) { func TestE2EFirejailedPackage(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"firejailed-package", "firejailed-package",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r) defaultPrepare(t, r)
execShouldNoError(t, r, "alr", "build", "-p", fmt.Sprintf("%s/firejailed-pkg", REPO_NAME_FOR_E2E_TESTS)) execShouldNoError(t, r, "alr", "build", "-p", fmt.Sprintf("%s/firejailed-pkg", REPO_NAME_FOR_E2E_TESTS))
execShouldError(t, r, "alr", "build", "-p", fmt.Sprintf("%s/firejailed-pkg-incorrect", REPO_NAME_FOR_E2E_TESTS)) execShouldError(t, r, "alr", "build", "-p", fmt.Sprintf("%s/firejailed-pkg-incorrect", REPO_NAME_FOR_E2E_TESTS))

View File

@@ -20,24 +20,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"time"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
expect "github.com/tailscale/goexpect"
) )
func TestE2EAlrFix(t *testing.T) { func TestE2EAlrFix(t *testing.T) {
dockerMultipleRun( runMatrixSuite(t, "run-fix", COMMON_SYSTEMS, func(t *testing.T, r capytest.Runner) {
t, r.Command("alr", "fix").
"run-fix", ExpectStderrContains("--> Done").
COMMON_SYSTEMS, ExpectSuccess().
func(t *testing.T, r e2e.Runnable) { Run(t)
runTestCommands(t, r, time.Second*30, []expect.Batcher{ })
&expect.BSnd{S: "alr fix\n"},
&expect.BExp{R: `--> Done`},
&expect.BSnd{S: "echo $?\n"},
&expect.BExp{R: `^0\n$`},
})
},
)
} }

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EGroupAndSummaryField(t *testing.T) { func TestE2EGroupAndSummaryField(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"group-and-summary-field", "group-and-summary-field",
RPM_SYSTEMS, RPM_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r) defaultPrepare(t, r)
execShouldNoError(t, r, "sh", "-c", "alr search --name test-group-and-summary --format \"{{.Group.Resolved}}\" | grep ^System/Base$") execShouldNoError(t, r, "sh", "-c", "alr search --name test-group-and-summary --format \"{{.Group.Resolved}}\" | grep ^System/Base$")
execShouldNoError(t, r, "sh", "-c", "alr search --name test-group-and-summary --format \"{{.Summary.Resolved}}\" | grep \"^Custom summary$\"") execShouldNoError(t, r, "sh", "-c", "alr search --name test-group-and-summary --format \"{{.Summary.Resolved}}\" | grep \"^Custom summary$\"")

View File

@@ -0,0 +1,40 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
//go:build e2e
package e2etests_test
import (
"testing"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue129RepoTomlImportTest(t *testing.T) {
runMatrixSuite(
t,
"issue-129-repo-toml-import-test",
COMMON_SYSTEMS,
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
r.Command("alr", "config", "get", "repos").
ExpectStdoutMatchesSnapshot().
Run(t)
},
)
}

View File

@@ -0,0 +1,63 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
//go:build e2e
package e2etests_test
import (
"fmt"
"testing"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue130Install(t *testing.T) {
runMatrixSuite(
t,
"alr install {repo}/{package}",
COMMON_SYSTEMS,
func(t *testing.T, r capytest.Runner) {
t.Parallel()
defaultPrepare(t, r)
r.Command("sudo", "alr", "in", fmt.Sprintf("%s/foo-pkg", REPO_NAME_FOR_E2E_TESTS)).
ExpectSuccess().
Run(t)
r.Command("sudo", "alr", "in", fmt.Sprintf("%s/bar-pkg", "NOT_REPO_NAME_FOR_E2E_TESTS")).
ExpectFailure().
Run(t)
},
)
runMatrixSuite(
t,
"alr install {package}+alr-{repo}",
COMMON_SYSTEMS,
func(t *testing.T, r capytest.Runner) {
t.Parallel()
defaultPrepare(t, r)
r.Command("sudo", "alr", "in", fmt.Sprintf("foo-pkg+alr-%s", REPO_NAME_FOR_E2E_TESTS)).
ExpectSuccess().
Run(t)
r.Command("sudo", "alr", "in", fmt.Sprintf("bar-pkg+alr-%s", "NOT_REPO_NAME_FOR_E2E_TESTS")).
ExpectFailure().
Run(t)
},
)
}

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EIssue32Interactive(t *testing.T) { func TestE2EIssue32Interactive(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"issue-32-interactive", "issue-32-interactive",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
execShouldNoError(t, r, "alr", "--interactive=false", "remove", "ca-certificates") execShouldNoError(t, r, "alr", "--interactive=false", "remove", "ca-certificates")
execShouldNoError(t, r, "sudo", "alr", "--interactive=false", "remove", "openssl") execShouldNoError(t, r, "sudo", "alr", "--interactive=false", "remove", "openssl")
execShouldNoError(t, r, "alr", "fix") execShouldNoError(t, r, "alr", "fix")

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EIssue41AutoreqSkiplist(t *testing.T) { func TestE2EIssue41AutoreqSkiplist(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"issue-41-autoreq-skiplist", "issue-41-autoreq-skiplist",
AUTOREQ_AUTOPROV_SYSTEMS, AUTOREQ_AUTOPROV_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r) defaultPrepare(t, r)
execShouldNoError(t, r, "alr", "build", "-p", "alr-repo/test-autoreq-autoprov") execShouldNoError(t, r, "alr", "build", "-p", "alr-repo/test-autoreq-autoprov")
execShouldNoError(t, r, "sh", "-c", "rpm -qp --requires *.rpm | grep \"^/bin/sh$\"") execShouldNoError(t, r, "sh", "-c", "rpm -qp --requires *.rpm | grep \"^/bin/sh$\"")

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EIssue50InstallMultiple(t *testing.T) { func TestE2EIssue50InstallMultiple(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"issue-50-install-multiple", "issue-50-install-multiple",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r) defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "in", "foo-pkg", "bar-pkg") execShouldNoError(t, r, "sudo", "alr", "in", "foo-pkg", "bar-pkg")
execShouldNoError(t, r, "cat", "/opt/foo") execShouldNoError(t, r, "cat", "/opt/foo")

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EIssue53LcAllCInfo(t *testing.T) { func TestE2EIssue53LcAllCInfo(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"issue-53-lc-all-c-info", "issue-53-lc-all-c-info",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r) defaultPrepare(t, r)
execShouldNoError(t, r, "bash", "-c", "LANG=C alr info foo-pkg") execShouldNoError(t, r, "bash", "-c", "LANG=C alr info foo-pkg")
}, },

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EIssue59RmCompletion(t *testing.T) { func TestE2EIssue59RmCompletion(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"issue-59-rm-completion", "issue-59-rm-completion",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r) defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "in", "foo-pkg", "bar-pkg") execShouldNoError(t, r, "sudo", "alr", "in", "foo-pkg", "bar-pkg")
execShouldNoError(t, r, "sh", "-c", "alr rm --generate-bash-completion | grep ^foo-pkg$") execShouldNoError(t, r, "sh", "-c", "alr rm --generate-bash-completion | grep ^foo-pkg$")

View File

@@ -0,0 +1,50 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
//go:build e2e
package e2etests_test
import (
"testing"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue62List(t *testing.T) {
runMatrixSuite(
t,
"issue-62-list",
COMMON_SYSTEMS,
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", "alr-repo", "bd26236cd7")
execShouldNoError(t, r, "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "in", "foo-pkg")
r.Command("alr", "list", "-I").
ExpectSuccess().
ExpectStdoutMatchesSnapshot().
Run(t)
r.Command("alr", "list").
ExpectSuccess().
ExpectStdoutMatchesSnapshot().
Run(t)
},
)
}

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EIssue72InstallWithDeps(t *testing.T) { func TestE2EIssue72InstallWithDeps(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"issue-72-install-with-deps", "issue-72-install-with-deps",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r) defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "in", "test-app-with-lib") execShouldNoError(t, r, "sudo", "alr", "in", "test-app-with-lib")
}, },

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EIssue74Upgradable(t *testing.T) { func TestE2EIssue74Upgradable(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"issue-74-upgradable", "issue-74-upgradable",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r) defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", "alr-repo", "bd26236cd7") execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", "alr-repo", "bd26236cd7")
execShouldNoError(t, r, "alr", "ref") execShouldNoError(t, r, "alr", "ref")

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EIssue75InstallWithDeps(t *testing.T) { func TestE2EIssue75InstallWithDeps(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"issue-75-ref-specify", "issue-75-ref-specify",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r) defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", "alr-repo", "bd26236cd7") execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", "alr-repo", "bd26236cd7")
execShouldNoError(t, r, "sh", "-c", "test $(alr list | wc -l) -eq 2 || exit 1") execShouldNoError(t, r, "sh", "-c", "test $(alr list | wc -l) -eq 2 || exit 1")

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func Test75SinglePackageRepo(t *testing.T) { func Test75SinglePackageRepo(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"issue-76-single-package-repo", "issue-76-single-package-repo",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
execShouldNoError(t, r, execShouldNoError(t, r,
"sudo", "sudo",
"alr", "alr",
@@ -38,8 +38,9 @@ func Test75SinglePackageRepo(t *testing.T) {
REPO_NAME_FOR_E2E_TESTS, REPO_NAME_FOR_E2E_TESTS,
"https://gitea.plemya-x.ru/Maks1mS/test-single-package-alr-repo.git", "https://gitea.plemya-x.ru/Maks1mS/test-single-package-alr-repo.git",
) )
execShouldNoError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", REPO_NAME_FOR_E2E_TESTS, "1075c918be") execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", REPO_NAME_FOR_E2E_TESTS, "1075c918be")
execShouldNoError(t, r, "alr", "ref") execShouldNoError(t, r, "alr", "fix")
execShouldNoError(t, r, "sudo", "alr", "in", "test-single-repo") execShouldNoError(t, r, "sudo", "alr", "in", "test-single-repo")
execShouldNoError(t, r, "sh", "-c", "alr list -U") execShouldNoError(t, r, "sh", "-c", "alr list -U")
execShouldNoError(t, r, "sh", "-c", "test $(alr list -U | wc -l) -eq 0 || exit 1") execShouldNoError(t, r, "sh", "-c", "test $(alr list -U | wc -l) -eq 0 || exit 1")

View File

@@ -0,0 +1,49 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
//go:build e2e
package e2etests_test
import (
"testing"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue78Mirrors(t *testing.T) {
runMatrixSuite(t, "issue-78-mirrors", COMMON_SYSTEMS, func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldNoError(t, r, "sudo", "alr", "repo", "set-url", REPO_NAME_FOR_E2E_TESTS, "https://example.com")
execShouldNoError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "clear", REPO_NAME_FOR_E2E_TESTS)
execShouldError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "rm", "--partial", REPO_NAME_FOR_E2E_TESTS, "gitea.plemya-x.ru/Maks1mS")
execShouldError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "rm", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "rm", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldError(t, r, "sudo", "alr", "ref")
},
)
}

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EIssue81MultiplePackages(t *testing.T) { func TestE2EIssue81MultiplePackages(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"issue-81-multiple-packages", "issue-81-multiple-packages",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r) defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "in", "first-package-with-dashes") execShouldNoError(t, r, "sudo", "alr", "in", "first-package-with-dashes")
execShouldNoError(t, r, "cat", "/opt/first-package") execShouldNoError(t, r, "cat", "/opt/first-package")

View File

@@ -21,15 +21,15 @@ package e2etests_test
import ( import (
"testing" "testing"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
) )
func TestE2EIssue91MultiplePackages(t *testing.T) { func TestE2EIssue91MultiplePackages(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"issue-91-set-repo-ref", "issue-91-set-repo-ref",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r) defaultPrepare(t, r)
execShouldError(t, r, "sudo", "alr", "repo", "set-ref") execShouldError(t, r, "sudo", "alr", "repo", "set-ref")
execShouldError(t, r, "sudo", "alr", "repo", "set-ref", "alr-repo") execShouldError(t, r, "sudo", "alr", "repo", "set-ref", "alr-repo")

View File

@@ -23,27 +23,26 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/efficientgo/e2e"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.alt-gnome.ru/capytest"
) )
func TestE2EIssue94TwiceBuild(t *testing.T) { func TestE2EIssue94TwiceBuild(t *testing.T) {
dockerMultipleRun( runMatrixSuite(
t, t,
"issue-94-twice-build", "issue-94-twice-build",
COMMON_SYSTEMS, COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) { func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r) defaultPrepare(t, r)
var stderr bytes.Buffer var stderr bytes.Buffer
err := r.Exec(
e2e.NewCommand("sudo", "alr", "in", "test-94-app"),
e2e.WithExecOptionStderr(&stderr),
)
assert.NoError(t, err, "command failed")
output := stderr.String() r.Command("sudo", "alr", "in", "test-94-app").
assert.Equal(t, 1, strings.Count(output, "Building package name=test-94-dep")) WithCaptureStderr(&stderr).
ExpectSuccess().
Run(t)
assert.Equal(t, 1, strings.Count(stderr.String(), "Building package name=test-94-dep"))
}, },
) )
} }

View File

@@ -0,0 +1,47 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
//go:build e2e
package e2etests_test
import (
"testing"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue95ConfigCommand(t *testing.T) {
runMatrixSuite(
t,
"issue-95-config-command",
COMMON_SYSTEMS,
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sh", "-c", "alr config show | grep \"autoPull: true\"")
execShouldNoError(t, r, "sh", "-c", "alr config get | grep \"autoPull: true\"")
execShouldError(t, r, "sh", "-c", "cat /etc/alr/alr.toml | grep \"autoPull\"")
execShouldNoError(t, r, "alr", "config", "get", "autoPull")
execShouldError(t, r, "alr", "config", "set", "autoPull")
execShouldNoError(t, r, "sudo", "alr", "config", "set", "autoPull", "false")
execShouldNoError(t, r, "sh", "-c", "alr config show | grep \"autoPull: false\"")
execShouldNoError(t, r, "sh", "-c", "alr config get | grep \"autoPull: false\"")
execShouldNoError(t, r, "sh", "-c", "cat /etc/alr/alr.toml | grep \"autoPull = false\"")
execShouldNoError(t, r, "alr", "config", "set", "autoPull", "true")
execShouldNoError(t, r, "sh", "-c", "cat /etc/alr/alr.toml | grep \"autoPull = true\"")
},
)
}

View File

@@ -20,25 +20,16 @@ package e2etests_test
import ( import (
"testing" "testing"
"time"
"github.com/efficientgo/e2e" "go.alt-gnome.ru/capytest"
expect "github.com/tailscale/goexpect"
) )
func TestE2EAlrVersion(t *testing.T) { func TestE2EAlrVersion(t *testing.T) {
dockerMultipleRun( runMatrixSuite(t, "version", COMMON_SYSTEMS, func(t *testing.T, r capytest.Runner) {
t, r.Command("alr", "version").
"check-version", ExpectStderrRegex(`^v\d+\.\d+\.\d+(?:-\d+-g[a-f0-9]+)?\n$`).
COMMON_SYSTEMS, ExpectStdoutEmpty().
func(t *testing.T, r e2e.Runnable) { ExpectSuccess().
runTestCommands(t, r, time.Second*10, []expect.Batcher{ Run(t)
&expect.BSnd{S: "alr version\n"}, })
&expect.BExp{R: `^v\d+\.\d+\.\d+(?:-\d+-g[a-f0-9]+)?\n$`},
&expect.BSnd{S: "echo $?\n"},
&expect.BExp{R: `^0\n$`},
})
},
)
} }

146
fix.go
View File

@@ -23,6 +23,7 @@ import (
"io/fs" "io/fs"
"log/slog" "log/slog"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
@@ -33,14 +34,28 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
) )
// execWithPrivileges выполняет команду напрямую если root или CI, иначе через sudo
func execWithPrivileges(name string, args ...string) *exec.Cmd {
isRoot := os.Geteuid() == 0
isCI := os.Getenv("CI") == "true"
if !isRoot && !isCI {
// Если не root и не в CI, используем sudo
allArgs := append([]string{name}, args...)
return exec.Command("sudo", allArgs...)
} else {
// Если root или в CI, запускаем напрямую
return exec.Command(name, args...)
}
}
func FixCmd() *cli.Command { func FixCmd() *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "fix", Name: "fix",
Usage: gotext.Get("Attempt to fix problems with ALR"), Usage: gotext.Get("Attempt to fix problems with ALR"),
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { // Команда выполняется от текущего пользователя
return err // При необходимости будет запрошен sudo для удаления файлов root
}
ctx := c.Context ctx := c.Context
@@ -57,37 +72,126 @@ func FixCmd() *cli.Command {
paths := cfg.GetPaths() paths := cfg.GetPaths()
slog.Info(gotext.Get("Clearing cache directory")) slog.Info(gotext.Get("Clearing cache and temporary directories"))
// Проверяем, существует ли директория кэша
dir, err := os.Open(paths.CacheDir) dir, err := os.Open(paths.CacheDir)
if err != nil { if err != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to open cache directory"), err) if os.IsNotExist(err) {
} // Директория не существует, просто создадим её позже
defer dir.Close() slog.Info(gotext.Get("Cache directory does not exist, will create it"))
} else {
return cliutils.FormatCliExit(gotext.Get("Unable to open cache directory"), err)
}
} else {
defer dir.Close()
entries, err := dir.Readdirnames(-1) entries, err := dir.Readdirnames(-1)
if err != nil { if err != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to read cache directory contents"), err) return cliutils.FormatCliExit(gotext.Get("Unable to read cache directory contents"), err)
}
for _, entry := range entries {
fullPath := filepath.Join(paths.CacheDir, entry)
if err := makeWritableRecursive(fullPath); err != nil {
slog.Debug("Failed to make path writable", "path", fullPath, "error", err)
} }
err = os.RemoveAll(fullPath) for _, entry := range entries {
fullPath := filepath.Join(paths.CacheDir, entry)
// Пробуем сделать файлы доступными для записи
if err := makeWritableRecursive(fullPath); err != nil {
slog.Debug("Failed to make path writable", "path", fullPath, "error", err)
}
// Пробуем удалить
err = os.RemoveAll(fullPath)
if err != nil {
// Если не получилось удалить, пробуем через sudo
slog.Warn(gotext.Get("Unable to remove cache item (%s) as current user, trying with sudo", entry))
sudoCmd := execWithPrivileges("rm", "-rf", fullPath)
if sudoErr := sudoCmd.Run(); sudoErr != nil {
// Если и через sudo не получилось, пропускаем с предупреждением
slog.Error(gotext.Get("Unable to remove cache item (%s)", entry), "error", err)
continue
}
}
}
}
// Очищаем временные директории
slog.Info(gotext.Get("Clearing temporary directory"))
tmpDir := "/tmp/alr"
if _, err := os.Stat(tmpDir); err == nil {
// Директория существует, пробуем очистить
err = os.RemoveAll(tmpDir)
if err != nil { if err != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to remove cache item (%s)", entry), err) // Если не получилось удалить, пробуем через sudo
slog.Warn(gotext.Get("Unable to remove temporary directory as current user, trying with sudo"))
sudoCmd := execWithPrivileges("rm", "-rf", tmpDir)
if sudoErr := sudoCmd.Run(); sudoErr != nil {
slog.Error(gotext.Get("Unable to remove temporary directory"), "error", err)
}
}
}
// Создаем базовый каталог /tmp/alr с владельцем root:wheel и правами 775
err = utils.EnsureTempDirWithRootOwner(tmpDir, 0o775)
if err != nil {
slog.Warn(gotext.Get("Unable to create temporary directory"), "error", err)
}
// Создаем каталог dl с правами для группы wheel
dlDir := filepath.Join(tmpDir, "dl")
err = utils.EnsureTempDirWithRootOwner(dlDir, 0o775)
if err != nil {
slog.Warn(gotext.Get("Unable to create download directory"), "error", err)
}
// Создаем каталог pkgs с правами для группы wheel
pkgsDir := filepath.Join(tmpDir, "pkgs")
err = utils.EnsureTempDirWithRootOwner(pkgsDir, 0o775)
if err != nil {
slog.Warn(gotext.Get("Unable to create packages directory"), "error", err)
}
// Исправляем права на все существующие файлы в /tmp/alr, если там что-то есть
if _, err := os.Stat(tmpDir); err == nil {
slog.Info(gotext.Get("Fixing permissions on temporary files"))
// Проверяем, есть ли файлы в директории
entries, err := os.ReadDir(tmpDir)
if err == nil && len(entries) > 0 {
fixCmd := execWithPrivileges("chown", "-R", "root:wheel", tmpDir)
if fixErr := fixCmd.Run(); fixErr != nil {
slog.Warn(gotext.Get("Unable to fix file ownership"), "error", fixErr)
}
fixCmd = execWithPrivileges("chmod", "-R", "2775", tmpDir)
if fixErr := fixCmd.Run(); fixErr != nil {
slog.Warn(gotext.Get("Unable to fix file permissions"), "error", fixErr)
}
} }
} }
slog.Info(gotext.Get("Rebuilding cache")) slog.Info(gotext.Get("Rebuilding cache"))
err = os.MkdirAll(paths.CacheDir, 0o755) // Пробуем создать директорию кэша
err = os.MkdirAll(paths.CacheDir, 0o775)
if err != nil { if err != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to create new cache directory"), err) // Если не получилось, пробуем через sudo с правильными правами для группы wheel
slog.Info(gotext.Get("Creating cache directory with sudo"))
sudoCmd := execWithPrivileges("mkdir", "-p", paths.CacheDir)
if sudoErr := sudoCmd.Run(); sudoErr != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to create new cache directory"), err)
}
// Устанавливаем права 775 и группу wheel
chmodCmd := execWithPrivileges("chmod", "775", paths.CacheDir)
if chmodErr := chmodCmd.Run(); chmodErr != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to set cache directory permissions"), chmodErr)
}
chgrpCmd := execWithPrivileges("chgrp", "wheel", paths.CacheDir)
if chgrpErr := chgrpCmd.Run(); chgrpErr != nil {
return cliutils.FormatCliExit(gotext.Get("Unable to set cache directory group"), chgrpErr)
}
} }
deps, err = appbuilder. deps, err = appbuilder.

23
gen.go
View File

@@ -61,6 +61,29 @@ func GenCmd() *cli.Command {
}) })
}, },
}, },
{
Name: "aur",
Usage: gotext.Get("Generate a ALR script for an AUR package"),
Flags: []cli.Flag{
&cli.StringFlag{
Name: "name",
Aliases: []string{"n"},
Required: true,
Usage: gotext.Get("Name of the AUR package"),
},
&cli.StringFlag{
Name: "version",
Aliases: []string{"v"},
Usage: gotext.Get("Version of the package (optional, uses latest if not specified)"),
},
},
Action: func(c *cli.Context) error {
return gen.AUR(os.Stdout, gen.AUROptions{
Name: c.String("name"),
Version: c.String("version"),
})
},
},
}, },
} }
} }

View File

@@ -0,0 +1,251 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package main
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"log"
"os"
"reflect"
"strings"
"text/template"
)
func resolvedStructGenerator(buf *bytes.Buffer, fields []*ast.Field) {
contentTemplate := template.Must(template.New("").Parse(`
type {{ .EntityNameLower }}Resolved struct {
{{ .StructFields }}
}
`))
var structFieldsBuilder strings.Builder
for _, field := range fields {
for _, name := range field.Names {
// Поле с типом
fieldTypeStr := exprToString(field.Type)
// Структура поля
var buf bytes.Buffer
buf.WriteString("\t")
buf.WriteString(name.Name)
buf.WriteString(" ")
buf.WriteString(fieldTypeStr)
// Обработка json-тега
jsonTag := ""
if field.Tag != nil {
raw := strings.Trim(field.Tag.Value, "`")
tag := reflect.StructTag(raw)
if val := tag.Get("json"); val != "" {
jsonTag = val
}
}
if jsonTag == "" {
jsonTag = strings.ToLower(name.Name)
}
buf.WriteString(fmt.Sprintf(" `json:\"%s\"`", jsonTag))
buf.WriteString("\n")
structFieldsBuilder.Write(buf.Bytes())
}
}
params := struct {
EntityNameLower string
StructFields string
}{
EntityNameLower: "package",
StructFields: structFieldsBuilder.String(),
}
err := contentTemplate.Execute(buf, params)
if err != nil {
log.Fatalf("execute template: %v", err)
}
}
func toResolvedFuncGenerator(buf *bytes.Buffer, fields []*ast.Field) {
contentTemplate := template.Must(template.New("").Parse(`
func {{ .EntityName }}ToResolved(src *{{ .EntityName }}) {{ .EntityNameLower }}Resolved {
return {{ .EntityNameLower }}Resolved{
{{ .Assignments }}
}
}
`))
var assignmentsBuilder strings.Builder
for _, field := range fields {
for _, name := range field.Names {
var assignBuf bytes.Buffer
assignBuf.WriteString("\t\t")
assignBuf.WriteString(name.Name)
assignBuf.WriteString(": ")
if isOverridableField(field.Type) {
assignBuf.WriteString(fmt.Sprintf("src.%s.Resolved()", name.Name))
} else {
assignBuf.WriteString(fmt.Sprintf("src.%s", name.Name))
}
assignBuf.WriteString(",\n")
assignmentsBuilder.Write(assignBuf.Bytes())
}
}
params := struct {
EntityName string
EntityNameLower string
Assignments string
}{
EntityName: "Package",
EntityNameLower: "package",
Assignments: assignmentsBuilder.String(),
}
err := contentTemplate.Execute(buf, params)
if err != nil {
log.Fatalf("execute template: %v", err)
}
}
func resolveFuncGenerator(buf *bytes.Buffer, fields []*ast.Field) {
contentTemplate := template.Must(template.New("").Parse(`
func Resolve{{ .EntityName }}(pkg *{{ .EntityName }}, overrides []string) {
{{.Code}}}
`))
var codeBuilder strings.Builder
for _, field := range fields {
for _, name := range field.Names {
if isOverridableField(field.Type) {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("\t\tpkg.%s.Resolve(overrides)\n", name.Name))
codeBuilder.Write(buf.Bytes())
}
}
}
params := struct {
EntityName string
Code string
}{
EntityName: "Package",
Code: codeBuilder.String(),
}
err := contentTemplate.Execute(buf, params)
if err != nil {
log.Fatalf("execute template: %v", err)
}
}
func main() {
path := os.Getenv("GOFILE")
if path == "" {
log.Fatal("GOFILE must be set")
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
if err != nil {
log.Fatalf("parsing file: %v", err)
}
entityName := "Package" // имя структуры, которую анализируем
found := false
fields := make([]*ast.Field, 0)
// Ищем структуру с нужным именем
for _, decl := range node.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
continue
}
for _, spec := range genDecl.Specs {
typeSpec := spec.(*ast.TypeSpec)
if typeSpec.Name.Name != entityName {
continue
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
fields = structType.Fields.List
found = true
}
}
if !found {
log.Fatalf("struct %s not found", entityName)
}
var buf bytes.Buffer
buf.WriteString("// DO NOT EDIT MANUALLY. This file is generated.\n")
buf.WriteString("package alrsh")
resolvedStructGenerator(&buf, fields)
toResolvedFuncGenerator(&buf, fields)
resolveFuncGenerator(&buf, fields)
// Форматируем вывод
formatted, err := format.Source(buf.Bytes())
if err != nil {
log.Fatalf("formatting: %v", err)
}
outPath := strings.TrimSuffix(path, ".go") + "_gen.go"
outFile, err := os.Create(outPath)
if err != nil {
log.Fatalf("create file: %v", err)
}
_, err = outFile.Write(formatted)
if err != nil {
log.Fatalf("writing output: %v", err)
}
outFile.Close()
}
func exprToString(expr ast.Expr) string {
if t, ok := expr.(*ast.IndexExpr); ok {
if ident, ok := t.X.(*ast.Ident); ok && ident.Name == "OverridableField" {
return exprToString(t.Index) // T
}
}
var buf bytes.Buffer
if err := format.Node(&buf, token.NewFileSet(), expr); err != nil {
return "<invalid>"
}
return buf.String()
}
func isOverridableField(expr ast.Expr) bool {
indexExpr, ok := expr.(*ast.IndexExpr)
if !ok {
return false
}
ident, ok := indexExpr.X.(*ast.Ident)
return ok && ident.Name == "OverridableField"
}

View File

@@ -0,0 +1,416 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package main
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"log"
"os"
"strings"
"text/template"
"unicode"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type MethodInfo struct {
Name string
Params []ParamInfo
Results []ResultInfo
EntityName string
}
type ParamInfo struct {
Name string
Type string
}
type ResultInfo struct {
Name string
Type string
Index int
}
func extractImports(node *ast.File) []string {
var imports []string
for _, imp := range node.Imports {
if imp.Path.Value != "" {
imports = append(imports, imp.Path.Value)
}
}
return imports
}
func output(path string, buf bytes.Buffer) {
formatted, err := format.Source(buf.Bytes())
if err != nil {
log.Fatalf("formatting: %v", err)
}
outPath := strings.TrimSuffix(path, ".go") + "_gen.go"
outFile, err := os.Create(outPath)
if err != nil {
log.Fatalf("create file: %v", err)
}
_, err = outFile.Write(formatted)
if err != nil {
log.Fatalf("writing output: %v", err)
}
outFile.Close()
}
func main() {
path := os.Getenv("GOFILE")
if path == "" {
log.Fatal("GOFILE must be set")
}
if len(os.Args) < 2 {
log.Fatal("At least one entity name must be provided")
}
entityNames := os.Args[1:]
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
if err != nil {
log.Fatalf("parsing file: %v", err)
}
packageName := node.Name.Name
// Find all specified entities
entityData := make(map[string][]*ast.Field)
for _, decl := range node.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
continue
}
for _, spec := range genDecl.Specs {
typeSpec := spec.(*ast.TypeSpec)
for _, entityName := range entityNames {
if typeSpec.Name.Name == entityName {
interfaceType, ok := typeSpec.Type.(*ast.InterfaceType)
if !ok {
log.Fatalf("entity %s is not an interface", entityName)
}
entityData[entityName] = interfaceType.Methods.List
}
}
}
}
// Verify all entities were found
for _, entityName := range entityNames {
if _, found := entityData[entityName]; !found {
log.Fatalf("interface %s not found", entityName)
}
}
var buf bytes.Buffer
buf.WriteString(`
// DO NOT EDIT MANUALLY. This file is generated.
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
`)
buf.WriteString(fmt.Sprintf("package %s\n", packageName))
// Generate base structures for all entities
baseStructs(&buf, entityNames, extractImports(node))
// Generate method-specific code for each entity
for _, entityName := range entityNames {
methods := parseMethodsFromFields(entityName, entityData[entityName])
argsGen(&buf, methods)
}
output(path, buf)
}
func parseMethodsFromFields(entityName string, fields []*ast.Field) []MethodInfo {
var methods []MethodInfo
for _, field := range fields {
if len(field.Names) == 0 {
continue
}
methodName := field.Names[0].Name
funcType, ok := field.Type.(*ast.FuncType)
if !ok {
continue
}
method := MethodInfo{
Name: methodName,
EntityName: entityName,
}
// Parse parameters, excluding context.Context
if funcType.Params != nil {
for i, param := range funcType.Params.List {
paramType := typeToString(param.Type)
// Skip context.Context parameters
if paramType == "context.Context" {
continue
}
if len(param.Names) == 0 {
method.Params = append(method.Params, ParamInfo{
Name: fmt.Sprintf("Arg%d", i),
Type: paramType,
})
} else {
for _, name := range param.Names {
method.Params = append(method.Params, ParamInfo{
Name: cases.Title(language.Und, cases.NoLower).String(name.Name),
Type: paramType,
})
}
}
}
}
// Parse results
if funcType.Results != nil {
resultIndex := 0
for _, result := range funcType.Results.List {
resultType := typeToString(result.Type)
if resultType == "error" {
continue // Skip error in response struct
}
if len(result.Names) == 0 {
method.Results = append(method.Results, ResultInfo{
Name: fmt.Sprintf("Result%d", resultIndex),
Type: resultType,
Index: resultIndex,
})
} else {
for _, name := range result.Names {
method.Results = append(method.Results, ResultInfo{
Name: cases.Title(language.Und, cases.NoLower).String(name.Name),
Type: resultType,
Index: resultIndex,
})
}
}
resultIndex++
}
}
methods = append(methods, method)
}
return methods
}
func argsGen(buf *bytes.Buffer, methods []MethodInfo) {
// Add template functions first
funcMap := template.FuncMap{
"lowerFirst": func(s string) string {
if len(s) == 0 {
return s
}
return strings.ToLower(s[:1]) + s[1:]
},
"zeroValue": func(typeName string) string {
typeName = strings.TrimSpace(typeName)
switch typeName {
case "string":
return "\"\""
case "int", "int8", "int16", "int32", "int64":
return "0"
case "uint", "uint8", "uint16", "uint32", "uint64":
return "0"
case "float32", "float64":
return "0.0"
case "bool":
return "false"
}
if strings.HasPrefix(typeName, "*") {
return "nil"
}
if strings.HasPrefix(typeName, "[]") ||
strings.HasPrefix(typeName, "map[") ||
strings.HasPrefix(typeName, "chan ") {
return "nil"
}
if typeName == "interface{}" {
return "nil"
}
// If external type: pkg.Type
if strings.Contains(typeName, ".") {
return typeName + "{}"
}
// If starts with uppercase — likely struct
if len(typeName) > 0 && unicode.IsUpper(rune(typeName[0])) {
return typeName + "{}"
}
return "nil"
},
}
argsTemplate := template.Must(template.New("args").Funcs(funcMap).Parse(`
{{range .}}
type {{.EntityName}}{{.Name}}Args struct {
{{range .Params}} {{.Name}} {{.Type}}
{{end}}}
type {{.EntityName}}{{.Name}}Resp struct {
{{range .Results}} {{.Name}} {{.Type}}
{{end}}}
func (s *{{.EntityName}}RPC) {{.Name}}(ctx context.Context, {{range $i, $p := .Params}}{{if $i}}, {{end}}{{lowerFirst $p.Name}} {{$p.Type}}{{end}}) ({{range $i, $r := .Results}}{{if $i}}, {{end}}{{$r.Type}}{{end}}{{if .Results}}, {{end}}error) {
var resp *{{.EntityName}}{{.Name}}Resp
err := s.client.Call("Plugin.{{.Name}}", &{{.EntityName}}{{.Name}}Args{
{{range .Params}} {{.Name}}: {{lowerFirst .Name}},
{{end}} }, &resp)
if err != nil {
return {{range $i, $r := .Results}}{{if $i}}, {{end}}{{zeroValue $r.Type}}{{end}}{{if .Results}}, {{end}}err
}
return {{range $i, $r := .Results}}{{if $i}}, {{end}}resp.{{$r.Name}}{{end}}{{if .Results}}, {{end}}nil
}
func (s *{{.EntityName}}RPCServer) {{.Name}}(args *{{.EntityName}}{{.Name}}Args, resp *{{.EntityName}}{{.Name}}Resp) error {
{{if .Results}}{{range $i, $r := .Results}}{{if $i}}, {{end}}{{lowerFirst $r.Name}}{{end}}, err := {{else}}err := {{end}}s.Impl.{{.Name}}(context.Background(),{{range $i, $p := .Params}}{{if $i}}, {{end}}args.{{$p.Name}}{{end}})
if err != nil {
return err
}
{{if .Results}}*resp = {{.EntityName}}{{.Name}}Resp{
{{range .Results}} {{.Name}}: {{lowerFirst .Name}},
{{end}} }
{{else}}*resp = {{.EntityName}}{{.Name}}Resp{}
{{end}}return nil
}
{{end}}
`))
err := argsTemplate.Execute(buf, methods)
if err != nil {
log.Fatalf("execute args template: %v", err)
}
}
func typeToString(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.Ident:
return t.Name
case *ast.StarExpr:
return "*" + typeToString(t.X)
case *ast.ArrayType:
return "[]" + typeToString(t.Elt)
case *ast.SelectorExpr:
xStr := typeToString(t.X)
if xStr == "context" && t.Sel.Name == "Context" {
return "context.Context"
}
return xStr + "." + t.Sel.Name
case *ast.InterfaceType:
return "interface{}"
default:
return "interface{}"
}
}
func baseStructs(buf *bytes.Buffer, entityNames, imports []string) {
// Ensure "context" is included in imports
updatedImports := imports
hasContext := false
for _, imp := range imports {
if strings.Contains(imp, `"context"`) {
hasContext = true
break
}
}
if !hasContext {
updatedImports = append(updatedImports, `"context"`)
}
contentTemplate := template.Must(template.New("").Parse(`
import (
"net/rpc"
"github.com/hashicorp/go-plugin"
{{range .Imports}} {{.}}
{{end}}
)
{{range .EntityNames}}
type {{ . }}Plugin struct {
Impl {{ . }}
}
type {{ . }}RPCServer struct {
Impl {{ . }}
}
type {{ . }}RPC struct {
client *rpc.Client
}
func (p *{{ . }}Plugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &{{ . }}RPC{client: c}, nil
}
func (p *{{ . }}Plugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &{{ . }}RPCServer{Impl: p.Impl}, nil
}
{{end}}
`))
err := contentTemplate.Execute(buf, struct {
EntityNames []string
Imports []string
}{
EntityNames: entityNames,
Imports: updatedImports,
})
if err != nil {
log.Fatalf("execute template: %v", err)
}
}

47
go.mod
View File

@@ -1,8 +1,6 @@
module gitea.plemya-x.ru/Plemya-x/ALR module gitea.plemya-x.ru/Plemya-x/ALR
go 1.23.0 go 1.24.4
toolchain go1.24.2
require ( require (
gitea.plemya-x.ru/Plemya-x/fakeroot v0.0.2-0.20250408104831-427aaa7713c3 gitea.plemya-x.ru/Plemya-x/fakeroot v0.0.2-0.20250408104831-427aaa7713c3
@@ -10,35 +8,39 @@ require (
github.com/PuerkitoBio/purell v1.2.0 github.com/PuerkitoBio/purell v1.2.0
github.com/alecthomas/chroma/v2 v2.9.1 github.com/alecthomas/chroma/v2 v2.9.1
github.com/bmatcuk/doublestar/v4 v4.8.1 github.com/bmatcuk/doublestar/v4 v4.8.1
github.com/caarlos0/env v3.5.0+incompatible
github.com/charmbracelet/bubbles v0.20.0 github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.2.4 github.com/charmbracelet/bubbletea v1.2.4
github.com/charmbracelet/lipgloss v1.0.0 github.com/charmbracelet/lipgloss v1.0.0
github.com/charmbracelet/log v0.4.0 github.com/charmbracelet/log v0.4.0
github.com/efficientgo/e2e v0.14.1-0.20240418111536-97db25a0c6c0
github.com/go-git/go-billy/v5 v5.6.0 github.com/go-git/go-billy/v5 v5.6.0
github.com/go-git/go-git/v5 v5.13.0 github.com/go-git/go-git/v5 v5.13.0
github.com/goccy/go-yaml v1.18.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/goreleaser/nfpm/v2 v2.41.0 github.com/goreleaser/nfpm/v2 v2.41.0
github.com/hashicorp/go-hclog v0.14.1 github.com/hashicorp/go-hclog v0.14.1
github.com/hashicorp/go-plugin v1.6.3 github.com/hashicorp/go-plugin v1.6.3
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08
github.com/knadh/koanf/parsers/toml/v2 v2.2.0
github.com/knadh/koanf/providers/confmap v1.0.0
github.com/knadh/koanf/providers/env v1.1.0
github.com/knadh/koanf/providers/file v1.2.0
github.com/knadh/koanf/v2 v2.2.1
github.com/leonelquinteros/gotext v1.7.0 github.com/leonelquinteros/gotext v1.7.0
github.com/mattn/go-isatty v0.0.20 github.com/mattn/go-isatty v0.0.20
github.com/mholt/archiver/v4 v4.0.0-alpha.8 github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/muesli/reflow v0.3.0 github.com/muesli/reflow v0.3.0
github.com/pelletier/go-toml/v2 v2.1.0 github.com/pelletier/go-toml/v2 v2.2.4
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/urfave/cli/v2 v2.25.7 github.com/urfave/cli/v2 v2.25.7
github.com/vmihailenco/msgpack/v5 v5.3.5 github.com/vmihailenco/msgpack/v5 v5.3.5
go.alt-gnome.ru/capytest v0.0.3-0.20250706082755-f20413e052f9
go.alt-gnome.ru/capytest/providers/podman v0.0.3-0.20250706082755-f20413e052f9
go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4 go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4
golang.org/x/crypto v0.36.0 golang.org/x/crypto v0.36.0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/sys v0.31.0 golang.org/x/sys v0.33.0
golang.org/x/text v0.23.0 golang.org/x/text v0.23.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.25.0 modernc.org/sqlite v1.25.0
mvdan.cc/sh/v3 v3.10.0 mvdan.cc/sh/v3 v3.10.0
xorm.io/xorm v1.3.9 xorm.io/xorm v1.3.9
@@ -62,7 +64,7 @@ require (
github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.4.5 // indirect github.com/charmbracelet/x/ansi v0.4.5 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudflare/circl v1.3.8 // indirect github.com/cloudflare/circl v1.6.1 // indirect
github.com/connesc/cipherio v0.2.1 // indirect github.com/connesc/cipherio v0.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/creack/pty v1.1.24 // indirect github.com/creack/pty v1.1.24 // indirect
@@ -71,20 +73,23 @@ require (
github.com/dlclark/regexp2 v1.10.0 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect github.com/dsnet/compress v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/efficientgo/core v1.0.0-rc.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.7.0 // indirect github.com/fatih/color v1.7.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gkampitakis/ciinfo v0.3.2 // indirect
github.com/gkampitakis/go-diff v1.3.2 // indirect
github.com/gkampitakis/go-snaps v0.5.13 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.8.1 // indirect github.com/goccy/go-json v0.8.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect github.com/golang/snappy v0.0.4 // indirect
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect
github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a // indirect github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a // indirect
github.com/google/uuid v1.4.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/goreleaser/chglog v0.6.1 // indirect github.com/goreleaser/chglog v0.6.1 // indirect
github.com/goreleaser/fileglob v1.3.0 // indirect github.com/goreleaser/fileglob v1.3.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect
@@ -98,7 +103,11 @@ require (
github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect github.com/klauspost/pgzip v1.2.6 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/maruel/natural v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect
@@ -117,13 +126,18 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shopspring/decimal v1.3.1 // indirect github.com/shopspring/decimal v1.3.1 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect github.com/skeema/knownhosts v1.3.0 // indirect
github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cast v1.7.1 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/therootcompany/xz v1.0.1 // indirect github.com/therootcompany/xz v1.0.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect github.com/ulikunitz/xz v0.5.12 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
@@ -135,10 +149,11 @@ require (
golang.org/x/sync v0.12.0 // indirect golang.org/x/sync v0.12.0 // indirect
golang.org/x/term v0.30.0 // indirect golang.org/x/term v0.30.0 // indirect
golang.org/x/tools v0.23.0 // indirect golang.org/x/tools v0.23.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/grpc v1.58.3 // indirect google.golang.org/grpc v1.67.3 // indirect
google.golang.org/protobuf v1.36.1 // indirect google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect

110
go.sum
View File

@@ -63,8 +63,6 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38= github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
@@ -77,15 +75,11 @@ github.com/bodgit/windows v1.0.0 h1:rLQ/XjsleZvx4fR1tB/UxQrK+SJ2OFHzfPjLWWOhDIA=
github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM= github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs=
github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y=
github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8= github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8=
github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk= github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk=
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE= github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE=
@@ -104,12 +98,13 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw= github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw=
github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA= github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
@@ -125,10 +120,6 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/efficientgo/core v1.0.0-rc.0 h1:jJoA0N+C4/knWYVZ6GrdHOtDyrg8Y/TR4vFpTaqTsqs=
github.com/efficientgo/core v1.0.0-rc.0/go.mod h1:kQa0V74HNYMfuJH6jiPiwNdpWXl4xd/K4tzlrcvYDQI=
github.com/efficientgo/e2e v0.14.1-0.20240418111536-97db25a0c6c0 h1:C/FNIs+MtAJgQYLJ9FX/ACFYyDRuLYoXTmueErrOJyA=
github.com/efficientgo/e2e v0.14.1-0.20240418111536-97db25a0c6c0/go.mod h1:plsKU0YHE9uX+7utvr7SiDtVBSHJyEfHRO4UnUgDmts=
github.com/elazarl/goproxy v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug= github.com/elazarl/goproxy v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug=
github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64= github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@@ -142,6 +133,14 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
github.com/gkampitakis/go-snaps v0.5.13 h1:Hhjmvv1WboSCxkR9iU2mj5PQ8tsz/y8ECGrIbjjPF8Q=
github.com/gkampitakis/go-snaps v0.5.13/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@@ -160,10 +159,14 @@ github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI= github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI=
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -196,8 +199,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@@ -210,8 +211,8 @@ github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a/go.mod h1:uqVAUVQ
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
@@ -252,8 +253,6 @@ github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3m
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o= github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@@ -271,6 +270,18 @@ github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek= github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/toml/v2 v2.2.0 h1:2nV7tHYJ5OZy2BynQ4mOJ6k5bDqbbCzRERLUKBytz3A=
github.com/knadh/koanf/parsers/toml/v2 v2.2.0/go.mod h1:JpjTeK1Ge1hVX0wbof5DMCuDBriR8bWgeQP98eeOZpI=
github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE=
github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A=
github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWyJgef9oQMSc=
github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY=
github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U=
github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
github.com/knadh/koanf/v2 v2.2.1 h1:jaleChtw85y3UdBnI0wCqcg1sj1gPoz6D3caGNHtrNE=
github.com/knadh/koanf/v2 v2.2.1/go.mod h1:PSFru3ufQgTsI7IF+95rf9s8XA1+aHxKuO/W+dPoHEY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -282,6 +293,8 @@ github.com/leonelquinteros/gotext v1.7.0 h1:jcJmF4AXqyamP7vuw2MMIKs+O3jAEmvrc5JQ
github.com/leonelquinteros/gotext v1.7.0/go.mod h1:qJdoQuERPpccw7L70uoU+K/BvTfRBHYsisCQyFLXyvw= github.com/leonelquinteros/gotext v1.7.0/go.mod h1:qJdoQuERPpccw7L70uoU+K/BvTfRBHYsisCQyFLXyvw=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@@ -300,8 +313,6 @@ github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6T
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM= github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM=
@@ -327,8 +338,6 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk= github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY= github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
@@ -339,25 +348,18 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0= github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.36.0 h1:78hJTing+BLYLjhXE+Z2BubeEymH5Lr0/Mt8FKkxxYo=
github.com/prometheus/common v0.36.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@@ -366,6 +368,7 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@@ -386,27 +389,31 @@ github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
@@ -425,6 +432,12 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8= gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8=
gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
go.alt-gnome.ru/capytest v0.0.2 h1:clmvIqmYS86hhA1rsvivSSPpfOFkJTpbn38EQP7I3E8=
go.alt-gnome.ru/capytest v0.0.2/go.mod h1:lvxPx3H6h+LPnStBFblgoT2wkjv0wbug3S14troykEg=
go.alt-gnome.ru/capytest v0.0.3-0.20250706082755-f20413e052f9 h1:NST+V5LV/eLgs0p6PsuvfHiZ4UrIWqftCdifO8zgg0g=
go.alt-gnome.ru/capytest v0.0.3-0.20250706082755-f20413e052f9/go.mod h1:qiM8LARP+JBZr5mrDoVylOoqjrN0MAzvZ21NR9qMc0Y=
go.alt-gnome.ru/capytest/providers/podman v0.0.3-0.20250706082755-f20413e052f9 h1:VZclgdJxARvhZ6PIWWW2hQ6Ge4XeE36pzUr/U/y62bE=
go.alt-gnome.ru/capytest/providers/podman v0.0.3-0.20250706082755-f20413e052f9/go.mod h1:Wpq1Ny3eMzADJpMJArA2TZGZbsviUBmawtEPcxnoerg=
go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4 h1:Ep54XceQlKhcCHl9awG+wWP4kz4kIP3c3Lzw/Gc/zwY= go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4 h1:Ep54XceQlKhcCHl9awG+wWP4kz4kIP3c3Lzw/Gc/zwY=
go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4/go.mod h1:/7PNW7nFnDR5W7UXZVc04gdVLR/wBNgkm33KgIz0OBk= go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4/go.mod h1:/7PNW7nFnDR5W7UXZVc04gdVLR/wBNgkm33KgIz0OBk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@@ -498,8 +511,6 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -537,8 +548,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@@ -599,8 +610,6 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@@ -614,8 +623,8 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -623,8 +632,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ= google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0= google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
@@ -644,7 +653,6 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

52
info.go
View File

@@ -23,15 +23,14 @@ import (
"fmt" "fmt"
"os" "os"
"github.com/goccy/go-yaml"
"github.com/jeandeaual/go-locale" "github.com/jeandeaual/go-locale"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" "gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
) )
@@ -48,9 +47,6 @@ func InfoCmd() *cli.Command {
}, },
}, },
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error { BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
ctx := c.Context ctx := c.Context
deps, err := appbuilder. deps, err := appbuilder.
@@ -74,9 +70,7 @@ func InfoCmd() *cli.Command {
return nil return nil
}), }),
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { // Запуск от текущего пользователя
return err
}
args := c.Args() args := c.Args()
if args.Len() < 1 { if args.Len() < 1 {
@@ -121,35 +115,27 @@ func InfoCmd() *cli.Command {
systemLang = "en" systemLang = "en"
} }
if !all { info, err := distro.ParseOSRelease(ctx)
info, err := distro.ParseOSRelease(ctx) if err != nil {
if err != nil { return cliutils.FormatCliExit(gotext.Get("Error parsing os-release file"), err)
return cliutils.FormatCliExit(gotext.Get("Error parsing os-release file"), err) }
} names, err = overrides.Resolve(
names, err = overrides.Resolve( info,
info, overrides.DefaultOpts.
overrides.DefaultOpts. WithLanguages([]string{systemLang}),
WithLanguages([]string{systemLang}), )
) if err != nil {
if err != nil { return cliutils.FormatCliExit(gotext.Get("Error resolving overrides"), err)
return cliutils.FormatCliExit(gotext.Get("Error resolving overrides"), err)
}
} }
for _, pkg := range pkgs { for _, pkg := range pkgs {
if !all { alrsh.ResolvePackage(&pkg, names)
alrsh.ResolvePackage(&pkg, names) view := alrsh.NewPackageView(pkg)
err = yaml.NewEncoder(os.Stdout).Encode(pkg) view.Resolved = !all
if err != nil { err = yaml.NewEncoder(os.Stdout, yaml.UseJSONMarshaler(), yaml.OmitEmpty()).Encode(view)
return cliutils.FormatCliExit(gotext.Get("Error encoding script variables"), err) if err != nil {
} return cliutils.FormatCliExit(gotext.Get("Error encoding script variables"), err)
} else {
err = yaml.NewEncoder(os.Stdout).Encode(pkg)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error encoding script variables"), err)
}
} }
fmt.Println("---") fmt.Println("---")
} }

View File

@@ -51,9 +51,6 @@ func InstallCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Command install expected at least 1 argument, got %d", args.Len()), nil) return cliutils.FormatCliExit(gotext.Get("Command install expected at least 1 argument, got %d", args.Len()), nil)
} }
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
installer, installerClose, err := build.GetSafeInstaller() installer, installerClose, err := build.GetSafeInstaller()
if err != nil { if err != nil {
@@ -61,9 +58,6 @@ func InstallCmd() *cli.Command {
} }
defer installerClose() defer installerClose()
if err := utils.ExitIfCantSetNoNewPrivs(); err != nil {
return err
}
scripter, scripterClose, err := build.GetSafeScriptExecutor() scripter, scripterClose, err := build.GetSafeScriptExecutor()
if err != nil { if err != nil {
@@ -116,9 +110,6 @@ func InstallCmd() *cli.Command {
return nil return nil
}), }),
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error { BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
ctx := c.Context ctx := c.Context
deps, err := appbuilder. deps, err := appbuilder.

View File

@@ -17,14 +17,8 @@
package main package main
import ( import (
"bufio"
"errors"
"fmt"
"log/slog" "log/slog"
"os" "os"
"os/exec"
"os/user"
"path/filepath"
"syscall" "syscall"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
@@ -36,7 +30,6 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger" "gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
@@ -52,9 +45,6 @@ func InternalBuildCmd() *cli.Command {
slog.Debug("start _internal-safe-script-executor", "uid", syscall.Getuid(), "gid", syscall.Getgid()) slog.Debug("start _internal-safe-script-executor", "uid", syscall.Getuid(), "gid", syscall.Getgid())
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
cfg := config.New() cfg := config.New()
err := cfg.Load() err := cfg.Load()
@@ -84,6 +74,40 @@ func InternalBuildCmd() *cli.Command {
} }
} }
func InternalReposCmd() *cli.Command {
return &cli.Command{
Name: "_internal-repos",
HideHelp: true,
Hidden: true,
Action: utils.RootNeededAction(func(ctx *cli.Context) error {
logger.SetupForGoPlugin()
deps, err := appbuilder.
New(ctx.Context).
WithConfig().
WithDB().
WithReposNoPull().
Build()
if err != nil {
return err
}
defer deps.Defer()
pluginCfg := build.GetPluginServeCommonConfig()
pluginCfg.Plugins = map[string]plugin.Plugin{
"repos": &build.ReposExecutorPlugin{
Impl: build.NewRepos(
deps.Repos,
),
},
}
plugin.Serve(pluginCfg)
return nil
}),
}
}
func InternalInstallCmd() *cli.Command { func InternalInstallCmd() *cli.Command {
return &cli.Command{ return &cli.Command{
Name: "_internal-installer", Name: "_internal-installer",
@@ -92,16 +116,7 @@ func InternalInstallCmd() *cli.Command {
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
logger.SetupForGoPlugin() logger.SetupForGoPlugin()
if err := utils.EnsureIsAlrUser(); err != nil { // Запуск от текущего пользователя, повышение прав будет через sudo при необходимости
return err
}
// Before escalating the rights, we made sure that
// this is an ALR user, so it looks safe.
err := utils.EscalateToRootUid()
if err != nil {
return cliutils.FormatCliExit("cannot escalate to root", err)
}
deps, err := appbuilder. deps, err := appbuilder.
New(c.Context). New(c.Context).
@@ -125,7 +140,7 @@ func InternalInstallCmd() *cli.Command {
plugin.Serve(&plugin.ServeConfig{ plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: build.HandshakeConfig, HandshakeConfig: build.HandshakeConfig,
Plugins: map[string]plugin.Plugin{ Plugins: map[string]plugin.Plugin{
"installer": &build.InstallerPlugin{ "installer": &build.InstallerExecutorPlugin{
Impl: build.NewInstaller( Impl: build.NewInstaller(
manager.Detect(), manager.Detect(),
), ),
@@ -138,143 +153,4 @@ func InternalInstallCmd() *cli.Command {
} }
} }
func Mount(target string) (string, func(), error) {
exe, err := os.Executable()
if err != nil {
return "", nil, fmt.Errorf("failed to get executable path: %w", err)
}
cmd := exec.Command(exe, "_internal-temporary-mount", target)
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return "", nil, fmt.Errorf("failed to get stdout pipe: %w", err)
}
stdinPipe, err := cmd.StdinPipe()
if err != nil {
return "", nil, fmt.Errorf("failed to get stdin pipe: %w", err)
}
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
return "", nil, fmt.Errorf("failed to start mount: %w", err)
}
scanner := bufio.NewScanner(stdoutPipe)
var mountPath string
if scanner.Scan() {
mountPath = scanner.Text()
}
if err := scanner.Err(); err != nil {
_ = cmd.Process.Kill()
return "", nil, fmt.Errorf("failed to read mount output: %w", err)
}
if mountPath == "" {
_ = cmd.Process.Kill()
return "", nil, errors.New("mount failed: no target path returned")
}
cleanup := func() {
slog.Debug("cleanup triggered")
_, _ = fmt.Fprintln(stdinPipe, "")
_ = cmd.Wait()
}
return mountPath, cleanup, nil
}
func InternalMountCmd() *cli.Command {
return &cli.Command{
Name: "_internal-temporary-mount",
HideHelp: true,
Hidden: true,
Action: func(c *cli.Context) error {
logger.SetupForGoPlugin()
sourceDir := c.Args().First()
u, err := user.Current()
if err != nil {
return cliutils.FormatCliExit("cannot get current user", err)
}
_, alrGid, err := utils.GetUidGidAlrUser()
if err != nil {
return cliutils.FormatCliExit("cannot get alr user", err)
}
if _, err := os.Stat(sourceDir); err != nil {
return cliutils.FormatCliExit(fmt.Sprintf("cannot read %s", sourceDir), err)
}
if err := utils.EnuseIsPrivilegedGroupMember(); err != nil {
return err
}
// Before escalating the rights, we made sure that
// 1. user in wheel group
// 2. user can access sourceDir
if err := utils.EscalateToRootUid(); err != nil {
return err
}
if err := syscall.Setgid(alrGid); err != nil {
return err
}
if err := os.MkdirAll(constants.AlrRunDir, 0o770); err != nil {
return cliutils.FormatCliExit(fmt.Sprintf("failed to create %s", constants.AlrRunDir), err)
}
if err := os.Chown(constants.AlrRunDir, 0, alrGid); err != nil {
return cliutils.FormatCliExit(fmt.Sprintf("failed to chown %s", constants.AlrRunDir), err)
}
targetDir := filepath.Join(constants.AlrRunDir, fmt.Sprintf("bindfs-%d", os.Getpid()))
// 0750: owner (root) and group (alr)
if err := os.MkdirAll(targetDir, 0o750); err != nil {
return cliutils.FormatCliExit("error creating bindfs target directory", err)
}
// chown AlrRunDir/mounts/bindfs-* to (root:alr),
// so alr user can access dir
if err := os.Chown(targetDir, 0, alrGid); err != nil {
return cliutils.FormatCliExit("failed to chown bindfs directory", err)
}
bindfsCmd := exec.Command(
"bindfs",
fmt.Sprintf("--map=%s/alr:@%s/@alr", u.Uid, u.Gid),
sourceDir,
targetDir,
)
bindfsCmd.Stderr = os.Stderr
if err := bindfsCmd.Run(); err != nil {
return cliutils.FormatCliExit("failed to strart bindfs", err)
}
fmt.Println(targetDir)
_, _ = bufio.NewReader(os.Stdin).ReadString('\n')
slog.Debug("start unmount", "dir", targetDir)
umountCmd := exec.Command("umount", targetDir)
umountCmd.Stderr = os.Stderr
if err := umountCmd.Run(); err != nil {
return cliutils.FormatCliExit(fmt.Sprintf("failed to unmount %s", targetDir), err)
}
if err := os.Remove(targetDir); err != nil {
return cliutils.FormatCliExit(fmt.Sprintf("error removing directory %s", targetDir), err)
}
return nil
},
}
}

View File

@@ -32,6 +32,7 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/stats"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
@@ -176,25 +177,6 @@ type ScriptResolverExecutor interface {
ResolveScript(ctx context.Context, pkg *alrsh.Package) *ScriptInfo ResolveScript(ctx context.Context, pkg *alrsh.Package) *ScriptInfo
} }
type ScriptExecutor interface {
ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error)
ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error)
PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error
ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *alrsh.ScriptFile,
varsOfPackages []*alrsh.Package,
repoDeps []string,
builtDeps []*BuiltDep,
basePkg string,
) ([]*BuiltDep, error)
}
type CacheExecutor interface { type CacheExecutor interface {
CheckForBuiltPackage(ctx context.Context, input *BuildInput, vars *alrsh.Package) (string, bool, error) CheckForBuiltPackage(ctx context.Context, input *BuildInput, vars *alrsh.Package) (string, bool, error)
} }
@@ -211,12 +193,6 @@ type CheckerExecutor interface {
) (bool, error) ) (bool, error)
} }
type InstallerExecutor interface {
InstallLocal(paths []string, opts *manager.Opts) error
Install(pkgs []string, opts *manager.Opts) error
RemoveAlreadyInstalled(pkgs []string) ([]string, error)
}
type SourcesInput struct { type SourcesInput struct {
Sources []string Sources []string
Checksums []string Checksums []string
@@ -344,9 +320,9 @@ func (b *Builder) BuildPackage(
} }
var builtDeps []*BuiltDep var builtDeps []*BuiltDep
var remainingVars []*alrsh.Package
if !input.opts.Clean { if !input.opts.Clean {
var remainingVars []*alrsh.Package
for _, vars := range varsOfPackages { for _, vars := range varsOfPackages {
builtPkgPath, ok, err := b.cacheExecutor.CheckForBuiltPackage(ctx, input, vars) builtPkgPath, ok, err := b.cacheExecutor.CheckForBuiltPackage(ctx, input, vars)
if err != nil { if err != nil {
@@ -355,6 +331,7 @@ func (b *Builder) BuildPackage(
if ok { if ok {
builtDeps = append(builtDeps, &BuiltDep{ builtDeps = append(builtDeps, &BuiltDep{
Path: builtPkgPath, Path: builtPkgPath,
Name: vars.Name,
}) })
} else { } else {
remainingVars = append(remainingVars, vars) remainingVars = append(remainingVars, vars)
@@ -362,12 +339,16 @@ func (b *Builder) BuildPackage(
} }
if len(remainingVars) == 0 { if len(remainingVars) == 0 {
slog.Info(gotext.Get("Using cached package"), "name", basePkg)
return builtDeps, nil return builtDeps, nil
} }
// Обновляем varsOfPackages только теми пакетами, которые нужно собрать
varsOfPackages = remainingVars
} }
slog.Debug("ViewScript") slog.Debug("ViewScript")
slog.Debug("", "varsOfPackages", varsOfPackages) slog.Debug("", "varsOfPackages", varsOfPackages[0])
err = b.scriptViewerExecutor.ViewScript(ctx, input, sf, basePkg) err = b.scriptViewerExecutor.ViewScript(ctx, input, sf, basePkg)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -408,7 +389,7 @@ func (b *Builder) BuildPackage(
sources, checksums = removeDuplicatesSources(sources, checksums) sources, checksums = removeDuplicatesSources(sources, checksums)
slog.Debug("installBuildDeps") slog.Debug("installBuildDeps")
alrBuildDeps, err := b.installBuildDeps(ctx, input, buildDepends) alrBuildDeps, installedBuildDeps, err := b.installBuildDeps(ctx, input, buildDepends)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -426,9 +407,19 @@ func (b *Builder) BuildPackage(
// We filter so as not to re-build what has already been built at the `installBuildDeps` stage. // We filter so as not to re-build what has already been built at the `installBuildDeps` stage.
var filteredDepends []string var filteredDepends []string
// Создаем набор подпакетов текущего мультипакета для исключения циклических зависимостей
currentPackageNames := make(map[string]struct{})
for _, pkg := range input.packages {
currentPackageNames[pkg] = struct{}{}
}
for _, d := range depends { for _, d := range depends {
if _, found := depNames[d]; !found { if _, found := depNames[d]; !found {
filteredDepends = append(filteredDepends, d) // Исключаем зависимости, которые являются подпакетами текущего мультипакета
if _, isCurrentPackage := currentPackageNames[d]; !isCurrentPackage {
filteredDepends = append(filteredDepends, d)
}
} }
} }
@@ -477,9 +468,40 @@ func (b *Builder) BuildPackage(
builtDeps = removeDuplicates(append(builtDeps, res...)) builtDeps = removeDuplicates(append(builtDeps, res...))
err = b.removeBuildDeps(ctx, input, installedBuildDeps)
if err != nil {
return nil, err
}
return builtDeps, nil return builtDeps, nil
} }
func (b *Builder) removeBuildDeps(ctx context.Context, input interface {
BuildOptsProvider
}, deps []string,
) error {
if len(deps) > 0 {
remove, err := cliutils.YesNoPrompt(ctx, gotext.Get("Would you like to remove the build dependencies?"), input.BuildOpts().Interactive, false)
if err != nil {
return err
}
if remove {
err = b.installerExecutor.Remove(
ctx,
deps,
&manager.Opts{
NoConfirm: !input.BuildOpts().Interactive,
},
)
if err != nil {
return err
}
}
}
return nil
}
type InstallPkgsArgs struct { type InstallPkgsArgs struct {
BuildArgs BuildArgs
AlrPkgs []alrsh.Package AlrPkgs []alrsh.Package
@@ -513,6 +535,7 @@ func (b *Builder) InstallALRPackages(
} }
err = b.installerExecutor.InstallLocal( err = b.installerExecutor.InstallLocal(
ctx,
GetBuiltPaths(res), GetBuiltPaths(res),
&manager.Opts{ &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive, NoConfirm: !input.BuildOpts().Interactive,
@@ -521,6 +544,13 @@ func (b *Builder) InstallALRPackages(
if err != nil { if err != nil {
return err return err
} }
// Отслеживание установки ALR пакетов
for _, dep := range res {
if stats.ShouldTrackPackage(dep.Name) {
stats.TrackInstallation(ctx, dep.Name, "upgrade")
}
}
} }
return nil return nil
@@ -545,11 +575,13 @@ func (b *Builder) BuildALRDeps(
repoDeps = notFound repoDeps = notFound
// Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез // Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез
pkgs := cliutils.FlattenPkgs( // Для зависимостей указываем isDependency = true
pkgs := cliutils.FlattenPkgsWithContext(
ctx, ctx,
found, found,
"install", "install",
input.BuildOpts().Interactive, input.BuildOpts().Interactive,
true,
) )
type item struct { type item struct {
pkg *alrsh.Package pkg *alrsh.Package
@@ -608,20 +640,22 @@ func (i *Builder) installBuildDeps(
PkgFormatProvider PkgFormatProvider
}, },
pkgs []string, pkgs []string,
) ([]*BuiltDep, error) { ) ([]*BuiltDep, []string, error) {
var builtDeps []*BuiltDep var builtDeps []*BuiltDep
var deps []string
var err error
if len(pkgs) > 0 { if len(pkgs) > 0 {
deps, err := i.installerExecutor.RemoveAlreadyInstalled(pkgs) deps, err = i.installerExecutor.RemoveAlreadyInstalled(ctx, pkgs)
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
builtDeps, err = i.InstallPkgs(ctx, input, deps) // Устанавливаем выбранные пакеты builtDeps, err = i.InstallPkgs(ctx, input, deps) // Устанавливаем выбранные пакеты
if err != nil { if err != nil {
return nil, err return nil, nil, err
} }
} }
return builtDeps, nil return builtDeps, deps, nil
} }
func (i *Builder) installOptDeps( func (i *Builder) installOptDeps(
@@ -634,7 +668,7 @@ func (i *Builder) installOptDeps(
pkgs []string, pkgs []string,
) ([]*BuiltDep, error) { ) ([]*BuiltDep, error) {
var builtDeps []*BuiltDep var builtDeps []*BuiltDep
optDeps, err := i.installerExecutor.RemoveAlreadyInstalled(pkgs) optDeps, err := i.installerExecutor.RemoveAlreadyInstalled(ctx, pkgs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -676,21 +710,35 @@ func (i *Builder) InstallPkgs(
} }
if len(builtDeps) > 0 { if len(builtDeps) > 0 {
err = i.installerExecutor.InstallLocal(GetBuiltPaths(builtDeps), &manager.Opts{ err = i.installerExecutor.InstallLocal(ctx, GetBuiltPaths(builtDeps), &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive, NoConfirm: !input.BuildOpts().Interactive,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Отслеживание установки локальных пакетов
for _, dep := range builtDeps {
if stats.ShouldTrackPackage(dep.Name) {
stats.TrackInstallation(ctx, dep.Name, "install")
}
}
} }
if len(repoDeps) > 0 { if len(repoDeps) > 0 {
err = i.installerExecutor.Install(repoDeps, &manager.Opts{ err = i.installerExecutor.Install(ctx, repoDeps, &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive, NoConfirm: !input.BuildOpts().Interactive,
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Отслеживание установки пакетов из репозитория
for _, pkg := range repoDeps {
if stats.ShouldTrackPackage(pkg) {
stats.TrackInstallation(ctx, pkg, "install")
}
}
} }
return builtDeps, nil return builtDeps, nil

View File

@@ -240,39 +240,10 @@ func createFirejailedBinary(
return nil, fmt.Errorf("failed to create wrapper script: %w", err) return nil, fmt.Errorf("failed to create wrapper script: %w", err)
} }
profile, err := getContentFromPath(dest, dirs.PkgDir) return buildContents(pkg, dirs, &[]string{
if err != nil { origFilePath,
return nil, err dest,
} })
bin, err := getContentFromPath(origFilePath, dirs.PkgDir)
if err != nil {
return nil, err
}
return []*files.Content{
bin,
profile,
}, nil
}
func getContentFromPath(path, base string) (*files.Content, error) {
absPath := filepath.Join(base, path)
fi, err := os.Lstat(absPath)
if err != nil {
return nil, fmt.Errorf("failed to get file info: %w", err)
}
return &files.Content{
Source: absPath,
Destination: path,
FileInfo: &files.ContentFileInfo{
MTime: fi.ModTime(),
Mode: fi.Mode(),
Size: fi.Size(),
},
}, nil
} }
func generateSafeName(destination string) (string, error) { func generateSafeName(destination string) (string, error) {

View File

@@ -17,6 +17,8 @@
package build package build
import ( import (
"context"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
) )
@@ -28,15 +30,19 @@ func NewInstaller(mgr manager.Manager) *Installer {
type Installer struct{ mgr manager.Manager } type Installer struct{ mgr manager.Manager }
func (i *Installer) InstallLocal(paths []string, opts *manager.Opts) error { func (i *Installer) InstallLocal(ctx context.Context, paths []string, opts *manager.Opts) error {
return i.mgr.InstallLocal(opts, paths...) return i.mgr.InstallLocal(opts, paths...)
} }
func (i *Installer) Install(pkgs []string, opts *manager.Opts) error { func (i *Installer) Install(ctx context.Context, pkgs []string, opts *manager.Opts) error {
return i.mgr.Install(opts, pkgs...) return i.mgr.Install(opts, pkgs...)
} }
func (i *Installer) RemoveAlreadyInstalled(pkgs []string) ([]string, error) { func (i *Installer) Remove(ctx context.Context, pkgs []string, opts *manager.Opts) error {
return i.mgr.Remove(opts, pkgs...)
}
func (i *Installer) RemoveAlreadyInstalled(ctx context.Context, pkgs []string) ([]string, error) {
filteredPackages := []string{} filteredPackages := []string{}
for _, dep := range pkgs { for _, dep := range pkgs {

142
internal/build/plugins.go Normal file
View File

@@ -0,0 +1,142 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package build
import (
"fmt"
"log/slog"
"os"
"os/exec"
"strings"
"sync"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
)
var pluginMap = map[string]plugin.Plugin{
"script-executor": &ScriptExecutorPlugin{},
"installer": &InstallerExecutorPlugin{},
"repos": &ReposExecutorPlugin{},
}
var HandshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "ALR_PLUGIN",
MagicCookieValue: "-",
}
func setCommonCmdEnv(cmd *exec.Cmd) {
cmd.Env = []string{
"HOME=" + os.Getenv("HOME"),
"LOGNAME=" + os.Getenv("USER"),
"USER=" + os.Getenv("USER"),
"PATH=/usr/bin:/bin:/usr/local/bin",
}
for _, env := range os.Environ() {
if strings.HasPrefix(env, "LANG=") ||
strings.HasPrefix(env, "LANGUAGE=") ||
strings.HasPrefix(env, "LC_") ||
strings.HasPrefix(env, "ALR_LOG_LEVEL=") {
cmd.Env = append(cmd.Env, env)
}
}
}
func GetPluginServeCommonConfig() *plugin.ServeConfig {
return &plugin.ServeConfig{
HandshakeConfig: HandshakeConfig,
Logger: hclog.New(&hclog.LoggerOptions{
Name: "plugin",
Output: os.Stderr,
Level: hclog.Trace,
JSONFormat: true,
DisableTime: true,
}),
}
}
func GetSafeInstaller() (InstallerExecutor, func(), error) {
return getSafeExecutor[InstallerExecutor]("_internal-installer", "installer")
}
func GetSafeScriptExecutor() (ScriptExecutor, func(), error) {
return getSafeExecutor[ScriptExecutor]("_internal-safe-script-executor", "script-executor")
}
func GetSafeReposExecutor() (ReposExecutor, func(), error) {
return getSafeExecutor[ReposExecutor]("_internal-repos", "repos")
}
func getSafeExecutor[T any](subCommand, pluginName string) (T, func(), error) {
var err error
executable, err := os.Executable()
if err != nil {
var zero T
return zero, nil, err
}
cmd := exec.Command(executable, subCommand)
setCommonCmdEnv(cmd)
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: HandshakeConfig,
Plugins: pluginMap,
Cmd: cmd,
Logger: logger.GetHCLoggerAdapter(),
SkipHostEnv: true,
UnixSocketConfig: &plugin.UnixSocketConfig{},
SyncStderr: os.Stderr,
})
rpcClient, err := client.Client()
if err != nil {
var zero T
return zero, nil, err
}
var cleanupOnce sync.Once
cleanup := func() {
cleanupOnce.Do(func() {
client.Kill()
})
}
defer func() {
if err != nil {
slog.Debug("close executor")
cleanup()
}
}()
raw, err := rpcClient.Dispense(pluginName)
if err != nil {
var zero T
return zero, nil, err
}
executor, ok := raw.(T)
if !ok {
var zero T
err = fmt.Errorf("dispensed object is not a %T (got %T)", zero, raw)
return zero, nil, err
}
return executor, cleanup, nil
}

View File

@@ -0,0 +1,60 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package build
import (
"context"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
)
//go:generate go run ../../generators/plugin-generator InstallerExecutor ScriptExecutor ReposExecutor
// The Executors interfaces must use context.Context as the first parameter,
// because the plugin-generator cannot generate code without it.
type InstallerExecutor interface {
InstallLocal(ctx context.Context, paths []string, opts *manager.Opts) error
Install(ctx context.Context, pkgs []string, opts *manager.Opts) error
Remove(ctx context.Context, pkgs []string, opts *manager.Opts) error
RemoveAlreadyInstalled(ctx context.Context, pkgs []string) ([]string, error)
}
type ScriptExecutor interface {
ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error)
ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error)
PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error
ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *alrsh.ScriptFile,
varsOfPackages []*alrsh.Package,
repoDeps []string,
builtDeps []*BuiltDep,
basePkg string,
) ([]*BuiltDep, error)
}
type ReposExecutor interface {
PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) (types.Repo, error)
}

View File

@@ -0,0 +1,369 @@
// DO NOT EDIT MANUALLY. This file is generated.
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package build
import (
"net/rpc"
"context"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
"github.com/hashicorp/go-plugin"
)
type InstallerExecutorPlugin struct {
Impl InstallerExecutor
}
type InstallerExecutorRPCServer struct {
Impl InstallerExecutor
}
type InstallerExecutorRPC struct {
client *rpc.Client
}
func (p *InstallerExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &InstallerExecutorRPC{client: c}, nil
}
func (p *InstallerExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &InstallerExecutorRPCServer{Impl: p.Impl}, nil
}
type ScriptExecutorPlugin struct {
Impl ScriptExecutor
}
type ScriptExecutorRPCServer struct {
Impl ScriptExecutor
}
type ScriptExecutorRPC struct {
client *rpc.Client
}
func (p *ScriptExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &ScriptExecutorRPC{client: c}, nil
}
func (p *ScriptExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &ScriptExecutorRPCServer{Impl: p.Impl}, nil
}
type ReposExecutorPlugin struct {
Impl ReposExecutor
}
type ReposExecutorRPCServer struct {
Impl ReposExecutor
}
type ReposExecutorRPC struct {
client *rpc.Client
}
func (p *ReposExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &ReposExecutorRPC{client: c}, nil
}
func (p *ReposExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &ReposExecutorRPCServer{Impl: p.Impl}, nil
}
type InstallerExecutorInstallLocalArgs struct {
Paths []string
Opts *manager.Opts
}
type InstallerExecutorInstallLocalResp struct {
}
func (s *InstallerExecutorRPC) InstallLocal(ctx context.Context, paths []string, opts *manager.Opts) error {
var resp *InstallerExecutorInstallLocalResp
err := s.client.Call("Plugin.InstallLocal", &InstallerExecutorInstallLocalArgs{
Paths: paths,
Opts: opts,
}, &resp)
if err != nil {
return err
}
return nil
}
func (s *InstallerExecutorRPCServer) InstallLocal(args *InstallerExecutorInstallLocalArgs, resp *InstallerExecutorInstallLocalResp) error {
err := s.Impl.InstallLocal(context.Background(), args.Paths, args.Opts)
if err != nil {
return err
}
*resp = InstallerExecutorInstallLocalResp{}
return nil
}
type InstallerExecutorInstallArgs struct {
Pkgs []string
Opts *manager.Opts
}
type InstallerExecutorInstallResp struct {
}
func (s *InstallerExecutorRPC) Install(ctx context.Context, pkgs []string, opts *manager.Opts) error {
var resp *InstallerExecutorInstallResp
err := s.client.Call("Plugin.Install", &InstallerExecutorInstallArgs{
Pkgs: pkgs,
Opts: opts,
}, &resp)
if err != nil {
return err
}
return nil
}
func (s *InstallerExecutorRPCServer) Install(args *InstallerExecutorInstallArgs, resp *InstallerExecutorInstallResp) error {
err := s.Impl.Install(context.Background(), args.Pkgs, args.Opts)
if err != nil {
return err
}
*resp = InstallerExecutorInstallResp{}
return nil
}
type InstallerExecutorRemoveArgs struct {
Pkgs []string
Opts *manager.Opts
}
type InstallerExecutorRemoveResp struct {
}
func (s *InstallerExecutorRPC) Remove(ctx context.Context, pkgs []string, opts *manager.Opts) error {
var resp *InstallerExecutorRemoveResp
err := s.client.Call("Plugin.Remove", &InstallerExecutorRemoveArgs{
Pkgs: pkgs,
Opts: opts,
}, &resp)
if err != nil {
return err
}
return nil
}
func (s *InstallerExecutorRPCServer) Remove(args *InstallerExecutorRemoveArgs, resp *InstallerExecutorRemoveResp) error {
err := s.Impl.Remove(context.Background(), args.Pkgs, args.Opts)
if err != nil {
return err
}
*resp = InstallerExecutorRemoveResp{}
return nil
}
type InstallerExecutorRemoveAlreadyInstalledArgs struct {
Pkgs []string
}
type InstallerExecutorRemoveAlreadyInstalledResp struct {
Result0 []string
}
func (s *InstallerExecutorRPC) RemoveAlreadyInstalled(ctx context.Context, pkgs []string) ([]string, error) {
var resp *InstallerExecutorRemoveAlreadyInstalledResp
err := s.client.Call("Plugin.RemoveAlreadyInstalled", &InstallerExecutorRemoveAlreadyInstalledArgs{
Pkgs: pkgs,
}, &resp)
if err != nil {
return nil, err
}
return resp.Result0, nil
}
func (s *InstallerExecutorRPCServer) RemoveAlreadyInstalled(args *InstallerExecutorRemoveAlreadyInstalledArgs, resp *InstallerExecutorRemoveAlreadyInstalledResp) error {
result0, err := s.Impl.RemoveAlreadyInstalled(context.Background(), args.Pkgs)
if err != nil {
return err
}
*resp = InstallerExecutorRemoveAlreadyInstalledResp{
Result0: result0,
}
return nil
}
type ScriptExecutorReadScriptArgs struct {
ScriptPath string
}
type ScriptExecutorReadScriptResp struct {
Result0 *alrsh.ScriptFile
}
func (s *ScriptExecutorRPC) ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error) {
var resp *ScriptExecutorReadScriptResp
err := s.client.Call("Plugin.ReadScript", &ScriptExecutorReadScriptArgs{
ScriptPath: scriptPath,
}, &resp)
if err != nil {
return nil, err
}
return resp.Result0, nil
}
func (s *ScriptExecutorRPCServer) ReadScript(args *ScriptExecutorReadScriptArgs, resp *ScriptExecutorReadScriptResp) error {
result0, err := s.Impl.ReadScript(context.Background(), args.ScriptPath)
if err != nil {
return err
}
*resp = ScriptExecutorReadScriptResp{
Result0: result0,
}
return nil
}
type ScriptExecutorExecuteFirstPassArgs struct {
Input *BuildInput
Sf *alrsh.ScriptFile
}
type ScriptExecutorExecuteFirstPassResp struct {
Result0 string
Result1 []*alrsh.Package
}
func (s *ScriptExecutorRPC) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error) {
var resp *ScriptExecutorExecuteFirstPassResp
err := s.client.Call("Plugin.ExecuteFirstPass", &ScriptExecutorExecuteFirstPassArgs{
Input: input,
Sf: sf,
}, &resp)
if err != nil {
return "", nil, err
}
return resp.Result0, resp.Result1, nil
}
func (s *ScriptExecutorRPCServer) ExecuteFirstPass(args *ScriptExecutorExecuteFirstPassArgs, resp *ScriptExecutorExecuteFirstPassResp) error {
result0, result1, err := s.Impl.ExecuteFirstPass(context.Background(), args.Input, args.Sf)
if err != nil {
return err
}
*resp = ScriptExecutorExecuteFirstPassResp{
Result0: result0,
Result1: result1,
}
return nil
}
type ScriptExecutorPrepareDirsArgs struct {
Input *BuildInput
BasePkg string
}
type ScriptExecutorPrepareDirsResp struct {
}
func (s *ScriptExecutorRPC) PrepareDirs(ctx context.Context, input *BuildInput, basePkg string) error {
var resp *ScriptExecutorPrepareDirsResp
err := s.client.Call("Plugin.PrepareDirs", &ScriptExecutorPrepareDirsArgs{
Input: input,
BasePkg: basePkg,
}, &resp)
if err != nil {
return err
}
return nil
}
func (s *ScriptExecutorRPCServer) PrepareDirs(args *ScriptExecutorPrepareDirsArgs, resp *ScriptExecutorPrepareDirsResp) error {
err := s.Impl.PrepareDirs(context.Background(), args.Input, args.BasePkg)
if err != nil {
return err
}
*resp = ScriptExecutorPrepareDirsResp{}
return nil
}
type ScriptExecutorExecuteSecondPassArgs struct {
Input *BuildInput
Sf *alrsh.ScriptFile
VarsOfPackages []*alrsh.Package
RepoDeps []string
BuiltDeps []*BuiltDep
BasePkg string
}
type ScriptExecutorExecuteSecondPassResp struct {
Result0 []*BuiltDep
}
func (s *ScriptExecutorRPC) ExecuteSecondPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile, varsOfPackages []*alrsh.Package, repoDeps []string, builtDeps []*BuiltDep, basePkg string) ([]*BuiltDep, error) {
var resp *ScriptExecutorExecuteSecondPassResp
err := s.client.Call("Plugin.ExecuteSecondPass", &ScriptExecutorExecuteSecondPassArgs{
Input: input,
Sf: sf,
VarsOfPackages: varsOfPackages,
RepoDeps: repoDeps,
BuiltDeps: builtDeps,
BasePkg: basePkg,
}, &resp)
if err != nil {
return nil, err
}
return resp.Result0, nil
}
func (s *ScriptExecutorRPCServer) ExecuteSecondPass(args *ScriptExecutorExecuteSecondPassArgs, resp *ScriptExecutorExecuteSecondPassResp) error {
result0, err := s.Impl.ExecuteSecondPass(context.Background(), args.Input, args.Sf, args.VarsOfPackages, args.RepoDeps, args.BuiltDeps, args.BasePkg)
if err != nil {
return err
}
*resp = ScriptExecutorExecuteSecondPassResp{
Result0: result0,
}
return nil
}
type ReposExecutorPullOneAndUpdateFromConfigArgs struct {
Repo *types.Repo
}
type ReposExecutorPullOneAndUpdateFromConfigResp struct {
Result0 types.Repo
}
func (s *ReposExecutorRPC) PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) (types.Repo, error) {
var resp *ReposExecutorPullOneAndUpdateFromConfigResp
err := s.client.Call("Plugin.PullOneAndUpdateFromConfig", &ReposExecutorPullOneAndUpdateFromConfigArgs{
Repo: repo,
}, &resp)
if err != nil {
return types.Repo{}, err
}
return resp.Result0, nil
}
func (s *ReposExecutorRPCServer) PullOneAndUpdateFromConfig(args *ReposExecutorPullOneAndUpdateFromConfigArgs, resp *ReposExecutorPullOneAndUpdateFromConfigResp) error {
result0, err := s.Impl.PullOneAndUpdateFromConfig(context.Background(), args.Repo)
if err != nil {
return err
}
*resp = ReposExecutorPullOneAndUpdateFromConfigResp{
Result0: result0,
}
return nil
}

View File

@@ -17,24 +17,21 @@
package build package build
import ( import (
"os" "context"
"os/exec"
"strings" "gitea.plemya-x.ru/Plemya-x/ALR/internal/repos"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
) )
func setCommonCmdEnv(cmd *exec.Cmd) { type reposExecutor struct{ r *repos.Repos }
cmd.Env = []string{
"HOME=/var/cache/alr", func NewRepos(r *repos.Repos) ReposExecutor {
"LOGNAME=alr", return &reposExecutor{r}
"USER=alr", }
"PATH=/usr/bin:/bin:/usr/local/bin",
} func (r *reposExecutor) PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) (types.Repo, error) {
for _, env := range os.Environ() { if err := r.r.PullOneAndUpdateFromConfig(ctx, repo); err != nil {
if strings.HasPrefix(env, "LANG=") || return *repo, err
strings.HasPrefix(env, "LANGUAGE=") || }
strings.HasPrefix(env, "LC_") || return *repo, nil
strings.HasPrefix(env, "ALR_LOG_LEVEL=") {
cmd.Env = append(cmd.Env, env)
}
}
} }

View File

@@ -1,150 +0,0 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package build
import (
"fmt"
"log/slog"
"net/rpc"
"os"
"os/exec"
"sync"
"syscall"
"github.com/hashicorp/go-plugin"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
)
type InstallerPlugin struct {
Impl InstallerExecutor
}
type InstallerRPC struct {
client *rpc.Client
}
type InstallerRPCServer struct {
Impl InstallerExecutor
}
type InstallArgs struct {
PackagesOrPaths []string
Opts *manager.Opts
}
func (r *InstallerRPC) InstallLocal(paths []string, opts *manager.Opts) error {
return r.client.Call("Plugin.InstallLocal", &InstallArgs{
PackagesOrPaths: paths,
Opts: opts,
}, nil)
}
func (s *InstallerRPCServer) InstallLocal(args *InstallArgs, reply *struct{}) error {
return s.Impl.InstallLocal(args.PackagesOrPaths, args.Opts)
}
func (r *InstallerRPC) Install(pkgs []string, opts *manager.Opts) error {
return r.client.Call("Plugin.Install", &InstallArgs{
PackagesOrPaths: pkgs,
Opts: opts,
}, nil)
}
func (s *InstallerRPCServer) Install(args *InstallArgs, reply *struct{}) error {
return s.Impl.Install(args.PackagesOrPaths, args.Opts)
}
func (r *InstallerRPC) RemoveAlreadyInstalled(paths []string) ([]string, error) {
var val []string
err := r.client.Call("Plugin.RemoveAlreadyInstalled", paths, &val)
return val, err
}
func (s *InstallerRPCServer) RemoveAlreadyInstalled(pkgs []string, res *[]string) error {
vars, err := s.Impl.RemoveAlreadyInstalled(pkgs)
if err != nil {
return err
}
*res = vars
return nil
}
func (p *InstallerPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &InstallerRPC{client: c}, nil
}
func (p *InstallerPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &InstallerRPCServer{Impl: p.Impl}, nil
}
func GetSafeInstaller() (InstallerExecutor, func(), error) {
var err error
executable, err := os.Executable()
if err != nil {
return nil, nil, err
}
cmd := exec.Command(executable, "_internal-installer")
setCommonCmdEnv(cmd)
slog.Debug("safe installer setup", "uid", syscall.Getuid(), "gid", syscall.Getgid())
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: HandshakeConfig,
Plugins: pluginMap,
Cmd: cmd,
Logger: logger.GetHCLoggerAdapter(),
SkipHostEnv: true,
UnixSocketConfig: &plugin.UnixSocketConfig{
Group: "alr",
},
SyncStderr: os.Stderr,
})
rpcClient, err := client.Client()
if err != nil {
return nil, nil, err
}
var cleanupOnce sync.Once
cleanup := func() {
cleanupOnce.Do(func() {
client.Kill()
})
}
defer func() {
if err != nil {
slog.Debug("close installer")
cleanup()
}
}()
raw, err := rpcClient.Dispense("installer")
if err != nil {
return nil, nil, err
}
executor, ok := raw.(InstallerExecutor)
if !ok {
err = fmt.Errorf("dispensed object is not a ScriptExecutor (got %T)", raw)
return nil, nil, err
}
return executor, cleanup, nil
}

View File

@@ -1,273 +0,0 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package build
import (
"context"
"fmt"
"log/slog"
"net/rpc"
"os"
"os/exec"
"sync"
"github.com/hashicorp/go-plugin"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
)
var HandshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "ALR_PLUGIN",
MagicCookieValue: "-",
}
type ScriptExecutorPlugin struct {
Impl ScriptExecutor
}
type ScriptExecutorRPCServer struct {
Impl ScriptExecutor
}
// =============================
//
// ReadScript
//
func (s *ScriptExecutorRPC) ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error) {
var resp *alrsh.ScriptFile
err := s.client.Call("Plugin.ReadScript", scriptPath, &resp)
return resp, err
}
func (s *ScriptExecutorRPCServer) ReadScript(scriptPath string, resp *alrsh.ScriptFile) error {
file, err := s.Impl.ReadScript(context.Background(), scriptPath)
if err != nil {
return err
}
*resp = *file
return nil
}
// =============================
//
// ExecuteFirstPass
//
type ExecuteFirstPassArgs struct {
Input *BuildInput
Sf *alrsh.ScriptFile
}
type ExecuteFirstPassResp struct {
BasePkg string
VarsOfPackages []*alrsh.Package
}
func (s *ScriptExecutorRPC) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error) {
var resp *ExecuteFirstPassResp
err := s.client.Call("Plugin.ExecuteFirstPass", &ExecuteFirstPassArgs{
Input: input,
Sf: sf,
}, &resp)
if err != nil {
return "", nil, err
}
return resp.BasePkg, resp.VarsOfPackages, nil
}
func (s *ScriptExecutorRPCServer) ExecuteFirstPass(args *ExecuteFirstPassArgs, resp *ExecuteFirstPassResp) error {
basePkg, varsOfPackages, err := s.Impl.ExecuteFirstPass(context.Background(), args.Input, args.Sf)
if err != nil {
return err
}
*resp = ExecuteFirstPassResp{
BasePkg: basePkg,
VarsOfPackages: varsOfPackages,
}
return nil
}
// =============================
//
// PrepareDirs
//
type PrepareDirsArgs struct {
Input *BuildInput
BasePkg string
}
func (s *ScriptExecutorRPC) PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error {
err := s.client.Call("Plugin.PrepareDirs", &PrepareDirsArgs{
Input: input,
BasePkg: basePkg,
}, nil)
if err != nil {
return err
}
return err
}
func (s *ScriptExecutorRPCServer) PrepareDirs(args *PrepareDirsArgs, reply *struct{}) error {
err := s.Impl.PrepareDirs(
context.Background(),
args.Input,
args.BasePkg,
)
if err != nil {
return err
}
return err
}
// =============================
//
// ExecuteSecondPass
//
type ExecuteSecondPassArgs struct {
Input *BuildInput
Sf *alrsh.ScriptFile
VarsOfPackages []*alrsh.Package
RepoDeps []string
BuiltDeps []*BuiltDep
BasePkg string
}
func (s *ScriptExecutorRPC) ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *alrsh.ScriptFile,
varsOfPackages []*alrsh.Package,
repoDeps []string,
builtDeps []*BuiltDep,
basePkg string,
) ([]*BuiltDep, error) {
var resp []*BuiltDep
err := s.client.Call("Plugin.ExecuteSecondPass", &ExecuteSecondPassArgs{
Input: input,
Sf: sf,
VarsOfPackages: varsOfPackages,
RepoDeps: repoDeps,
BuiltDeps: builtDeps,
BasePkg: basePkg,
}, &resp)
if err != nil {
return nil, err
}
return resp, nil
}
func (s *ScriptExecutorRPCServer) ExecuteSecondPass(args *ExecuteSecondPassArgs, resp *[]*BuiltDep) error {
res, err := s.Impl.ExecuteSecondPass(
context.Background(),
args.Input,
args.Sf,
args.VarsOfPackages,
args.RepoDeps,
args.BuiltDeps,
args.BasePkg,
)
if err != nil {
return err
}
*resp = res
return err
}
//
// ============================
//
func (p *ScriptExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &ScriptExecutorRPCServer{Impl: p.Impl}, nil
}
func (p *ScriptExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &ScriptExecutorRPC{client: c}, nil
}
type ScriptExecutorRPC struct {
client *rpc.Client
}
var pluginMap = map[string]plugin.Plugin{
"script-executor": &ScriptExecutorPlugin{},
"installer": &InstallerPlugin{},
}
func GetSafeScriptExecutor() (ScriptExecutor, func(), error) {
var err error
executable, err := os.Executable()
if err != nil {
return nil, nil, err
}
cmd := exec.Command(executable, "_internal-safe-script-executor")
setCommonCmdEnv(cmd)
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: HandshakeConfig,
Plugins: pluginMap,
Cmd: cmd,
Logger: logger.GetHCLoggerAdapter(),
SkipHostEnv: true,
UnixSocketConfig: &plugin.UnixSocketConfig{
Group: "alr",
},
SyncStderr: os.Stderr,
})
rpcClient, err := client.Client()
if err != nil {
return nil, nil, err
}
var cleanupOnce sync.Once
cleanup := func() {
cleanupOnce.Do(func() {
client.Kill()
})
}
defer func() {
if err != nil {
slog.Debug("close script-executor")
cleanup()
}
}()
raw, err := rpcClient.Dispense("script-executor")
if err != nil {
return nil, nil, err
}
executor, ok := raw.(ScriptExecutor)
if !ok {
err = fmt.Errorf("dispensed object is not a ScriptExecutor (got %T)", raw)
return nil, nil, err
}
return executor, cleanup, nil
}

View File

@@ -23,8 +23,9 @@ import (
"os" "os"
"strings" "strings"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dl" "gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dlcache" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
) )
type SourceDownloader struct { type SourceDownloader struct {
@@ -74,7 +75,9 @@ func (s *SourceDownloader) DownloadSources(
} }
} }
opts.DlCache = dlcache.New(s.cfg) // Используем временную директорию для загрузок
// dlcache.New добавит свой подкаталог "dl" внутри
opts.DlCache = dlcache.New(constants.TempDir)
err := dl.Download(ctx, opts) err := dl.Download(ctx, opts)
if err != nil { if err != nil {

View File

@@ -19,6 +19,7 @@ package build
import ( import (
"fmt" "fmt"
"io" "io"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@@ -40,6 +41,7 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" "gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
@@ -47,15 +49,22 @@ import (
// Функция prepareDirs подготавливает директории для сборки. // Функция prepareDirs подготавливает директории для сборки.
func prepareDirs(dirs types.Directories) error { func prepareDirs(dirs types.Directories) error {
err := os.RemoveAll(dirs.BaseDir) // Удаляем базовую директорию, если она существует // Пробуем удалить базовую директорию, если она существует
err := os.RemoveAll(dirs.BaseDir)
if err != nil {
// Если не можем удалить (например, принадлежит root), логируем и продолжаем
// Новые директории будут созданы или перезаписаны
slog.Debug("Failed to remove base directory", "path", dirs.BaseDir, "error", err)
}
// Создаем директории с правильным владельцем для /tmp/alr с setgid битом
err = utils.EnsureTempDirWithRootOwner(dirs.SrcDir, 0o2775)
if err != nil { if err != nil {
return err return err
} }
err = os.MkdirAll(dirs.SrcDir, 0o755) // Создаем директорию для источников
if err != nil { // Создаем директорию для пакетов с setgid битом
return err return utils.EnsureTempDirWithRootOwner(dirs.PkgDir, 0o2775)
}
return os.MkdirAll(dirs.PkgDir, 0o755) // Создаем директорию для пакетов
} }
// Функция buildContents создает секцию содержимого пакета, которая содержит файлы, // Функция buildContents создает секцию содержимого пакета, которая содержит файлы,

View File

@@ -103,22 +103,62 @@ func ShowScript(path, name, style string) error {
// FlattenPkgs attempts to flatten the a map of slices of packages into a single slice // FlattenPkgs attempts to flatten the a map of slices of packages into a single slice
// of packages by prompting the user if multiple packages match. // of packages by prompting the user if multiple packages match.
func FlattenPkgs(ctx context.Context, found map[string][]alrsh.Package, verb string, interactive bool) []alrsh.Package { func FlattenPkgs(ctx context.Context, found map[string][]alrsh.Package, verb string, interactive bool) []alrsh.Package {
return FlattenPkgsWithContext(ctx, found, verb, interactive, false)
}
// FlattenPkgsWithContext расширенная версия FlattenPkgs с контекстом обработки зависимостей
func FlattenPkgsWithContext(ctx context.Context, found map[string][]alrsh.Package, verb string, interactive bool, isDependency bool) []alrsh.Package {
var outPkgs []alrsh.Package var outPkgs []alrsh.Package
for _, pkgs := range found { for _, pkgs := range found {
if len(pkgs) > 1 && interactive { if len(pkgs) > 1 {
choice, err := PkgPrompt(ctx, pkgs, verb, interactive) // Проверяем, являются ли пакеты подпакетами одного мультипакета
if err != nil { if isMultiPackage(pkgs) && verb == "install" {
slog.Error(gotext.Get("Error prompting for choice of package")) // Для мультипакетов при установке ВСЕГДА берем все подпакеты без выбора
os.Exit(1) // Это правильное поведение как для прямой установки, так и для зависимостей
outPkgs = append(outPkgs, pkgs...)
} else if interactive {
// Для разных пакетов с одинаковым именем - показываем меню выбора
choice, err := PkgPrompt(ctx, pkgs, verb, interactive)
if err != nil {
slog.Error(gotext.Get("Error prompting for choice of package"))
os.Exit(1)
}
outPkgs = append(outPkgs, choice)
} else {
// Если не интерактивный режим - берем первый
outPkgs = append(outPkgs, pkgs[0])
} }
outPkgs = append(outPkgs, choice) } else {
} else if len(pkgs) == 1 || !interactive { // Если только один пакет - берем его
outPkgs = append(outPkgs, pkgs[0]) outPkgs = append(outPkgs, pkgs[0])
} }
} }
return outPkgs return outPkgs
} }
// isMultiPackage проверяет, являются ли пакеты подпакетами одного мультипакета
func isMultiPackage(pkgs []alrsh.Package) bool {
if len(pkgs) <= 1 {
return false
}
// Проверяем, что у всех пакетов одинаковый BasePkgName и Repository
firstBasePkg := pkgs[0].BasePkgName
firstRepo := pkgs[0].Repository
if firstBasePkg == "" {
return false // Не мультипакет
}
for _, pkg := range pkgs[1:] {
if pkg.BasePkgName != firstBasePkg || pkg.Repository != firstRepo {
return false
}
}
return true
}
// PkgPrompt asks the user to choose between multiple packages. // PkgPrompt asks the user to choose between multiple packages.
func PkgPrompt(ctx context.Context, options []alrsh.Package, verb string, interactive bool) (alrsh.Package, error) { func PkgPrompt(ctx context.Context, options []alrsh.Package, verb string, interactive bool) (alrsh.Package, error) {
if !interactive { if !interactive {

View File

@@ -20,13 +20,15 @@
package config package config
import ( import (
"log/slog" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"reflect"
"github.com/caarlos0/env" "github.com/goccy/go-yaml"
"github.com/pelletier/go-toml/v2" "github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/v2"
ktoml "github.com/knadh/koanf/parsers/toml/v2"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants" "gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
@@ -35,127 +37,229 @@ import (
type ALRConfig struct { type ALRConfig struct {
cfg *types.Config cfg *types.Config
paths *Paths paths *Paths
}
var defaultConfig = &types.Config{ System *SystemConfig
RootCmd: "sudo", env *EnvConfig
UseRootCmd: true,
PagerStyle: "native",
IgnorePkgUpdates: []string{},
AutoPull: true,
Repos: []types.Repo{},
} }
func New() *ALRConfig { func New() *ALRConfig {
return &ALRConfig{} return &ALRConfig{
System: NewSystemConfig(),
env: NewEnvConfig(),
}
} }
func readConfig(path string) (*types.Config, error) { func defaultConfigKoanf() *koanf.Koanf {
file, err := os.Open(path) k := koanf.New(".")
if err != nil { defaults := map[string]interface{}{
return nil, err "rootCmd": "sudo",
"useRootCmd": true,
"pagerStyle": "native",
"ignorePkgUpdates": []string{},
"logLevel": "info",
"autoPull": true,
"updateSystemOnUpgrade": false,
"repos": []types.Repo{
{
Name: "alr-default",
URL: "https://gitea.plemya-x.ru/Plemya-x/alr-default.git",
},
},
} }
defer file.Close() if err := k.Load(confmap.Provider(defaults, "."), nil); err != nil {
panic(k)
config := types.Config{}
if err := toml.NewDecoder(file).Decode(&config); err != nil {
return nil, err
}
return &config, nil
}
func mergeStructs(dst, src interface{}) {
srcVal := reflect.ValueOf(src)
if srcVal.IsNil() {
return
}
srcVal = srcVal.Elem()
dstVal := reflect.ValueOf(dst).Elem()
for i := range srcVal.NumField() {
srcField := srcVal.Field(i)
srcFieldName := srcVal.Type().Field(i).Name
dstField := dstVal.FieldByName(srcFieldName)
if dstField.IsValid() && dstField.CanSet() {
dstField.Set(srcField)
}
} }
return k
} }
func (c *ALRConfig) Load() error { func (c *ALRConfig) Load() error {
systemConfig, err := readConfig( config := types.Config{}
constants.SystemConfigPath,
) merged := koanf.New(".")
if err != nil {
slog.Debug("Cannot read system config", "err", err) if err := c.System.Load(); err != nil {
return fmt.Errorf("failed to load system config: %w", err)
} }
config := &types.Config{} if err := c.env.Load(); err != nil {
return fmt.Errorf("failed to load env config: %w", err)
mergeStructs(config, defaultConfig)
mergeStructs(config, systemConfig)
err = env.Parse(config)
if err != nil {
return err
} }
c.cfg = config systemK := c.System.koanf()
envK := c.env.koanf()
if err := merged.Merge(defaultConfigKoanf()); err != nil {
return fmt.Errorf("failed to merge default config: %w", err)
}
if err := merged.Merge(systemK); err != nil {
return fmt.Errorf("failed to merge system config: %w", err)
}
if err := merged.Merge(envK); err != nil {
return fmt.Errorf("failed to merge env config: %w", err)
}
if err := merged.Unmarshal("", &config); err != nil {
return fmt.Errorf("failed to unmarshal merged config: %w", err)
}
c.cfg = &config
c.paths = &Paths{} c.paths = &Paths{}
c.paths.UserConfigPath = constants.SystemConfigPath c.paths.UserConfigPath = constants.SystemConfigPath
c.paths.CacheDir = constants.SystemCachePath c.paths.CacheDir = constants.SystemCachePath
c.paths.RepoDir = filepath.Join(c.paths.CacheDir, "repo") c.paths.RepoDir = filepath.Join(c.paths.CacheDir, "repo")
c.paths.PkgsDir = filepath.Join(c.paths.CacheDir, "pkgs") c.paths.PkgsDir = filepath.Join(constants.TempDir, "pkgs") // Перемещаем в /tmp/alr/pkgs
c.paths.DBPath = filepath.Join(c.paths.CacheDir, "db") c.paths.DBPath = filepath.Join(c.paths.CacheDir, "alr.db")
// c.initPaths()
// Проверяем существование кэш-директории, но не пытаемся создать
if _, err := os.Stat(c.paths.CacheDir); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("failed to check cache directory: %w", err)
}
}
// Выполняем миграцию конфигурации при необходимости
if err := c.migrateConfig(); err != nil {
return fmt.Errorf("failed to migrate config: %w", err)
}
return nil return nil
} }
func (c *ALRConfig) RootCmd() string { func (c *ALRConfig) ToYAML() (string, error) {
return c.cfg.RootCmd data, err := yaml.Marshal(c.cfg)
}
func (c *ALRConfig) PagerStyle() string {
return c.cfg.PagerStyle
}
func (c *ALRConfig) AutoPull() bool {
return c.cfg.AutoPull
}
func (c *ALRConfig) Repos() []types.Repo {
return c.cfg.Repos
}
func (c *ALRConfig) SetRepos(repos []types.Repo) {
c.cfg.Repos = repos
}
func (c *ALRConfig) IgnorePkgUpdates() []string {
return c.cfg.IgnorePkgUpdates
}
func (c *ALRConfig) LogLevel() string {
return c.cfg.LogLevel
}
func (c *ALRConfig) UseRootCmd() bool {
return c.cfg.UseRootCmd
}
func (c *ALRConfig) GetPaths() *Paths {
return c.paths
}
func (c *ALRConfig) SaveUserConfig() error {
f, err := os.Create(c.paths.UserConfigPath)
if err != nil { if err != nil {
return err return "", err
}
return string(data), nil
}
func (c *ALRConfig) migrateConfig() error {
// Проверяем, существует ли конфигурационный файл
if _, err := os.Stat(constants.SystemConfigPath); os.IsNotExist(err) {
// Если файла нет, создаем полный конфигурационный файл с дефолтными значениями
if err := c.createDefaultConfig(); err != nil {
// Если не удается создать конфиг, это не критично - продолжаем работу
// но выводим предупреждение
fmt.Fprintf(os.Stderr, "Предупреждение: не удалось создать конфигурационный файл %s: %v\n", constants.SystemConfigPath, err)
return nil
}
} else {
// Если файл существует, проверяем, есть ли в нем новая опция
if !c.System.k.Exists("updateSystemOnUpgrade") {
// Если опции нет, добавляем ее со значением по умолчанию
c.System.SetUpdateSystemOnUpgrade(false)
// Сохраняем обновленную конфигурацию
if err := c.System.Save(); err != nil {
// Если не удается сохранить - это не критично, продолжаем работу
return nil
}
}
} }
return toml.NewEncoder(f).Encode(c.cfg) return nil
} }
func (c *ALRConfig) createDefaultConfig() error {
// Проверяем, запущен ли процесс от root
if os.Getuid() != 0 {
// Если не root, пытаемся запустить создание конфига с повышением привилегий
return c.createDefaultConfigWithPrivileges()
}
// Если уже root, создаем конфиг напрямую
return c.doCreateDefaultConfig()
}
func (c *ALRConfig) createDefaultConfigWithPrivileges() error {
// Если useRootCmd отключен, просто пытаемся создать без повышения привилегий
if !c.cfg.UseRootCmd {
return c.doCreateDefaultConfig()
}
// Определяем команду для повышения привилегий
rootCmd := c.cfg.RootCmd
if rootCmd == "" {
rootCmd = "sudo" // fallback
}
// Создаем временный файл с дефолтной конфигурацией
tmpFile, err := os.CreateTemp("", "alr-config-*.toml")
if err != nil {
return fmt.Errorf("не удалось создать временный файл: %w", err)
}
defer os.Remove(tmpFile.Name())
defer tmpFile.Close()
// Генерируем дефолтную конфигурацию во временный файл
defaults := defaultConfigKoanf()
tempSystemConfig := &SystemConfig{k: defaults}
bytes, err := tempSystemConfig.k.Marshal(ktoml.Parser())
if err != nil {
return fmt.Errorf("не удалось сериализовать конфигурацию: %w", err)
}
if _, err := tmpFile.Write(bytes); err != nil {
return fmt.Errorf("не удалось записать во временный файл: %w", err)
}
tmpFile.Close()
// Используем команду повышения привилегий для создания директории и копирования файла
// Создаем директорию с правами
configDir := filepath.Dir(constants.SystemConfigPath)
mkdirCmd := exec.Command(rootCmd, "mkdir", "-p", configDir)
if err := mkdirCmd.Run(); err != nil {
return fmt.Errorf("не удалось создать директорию %s: %w", configDir, err)
}
// Копируем файл в нужное место
cpCmd := exec.Command(rootCmd, "cp", tmpFile.Name(), constants.SystemConfigPath)
if err := cpCmd.Run(); err != nil {
return fmt.Errorf("не удалось скопировать конфигурацию в %s: %w", constants.SystemConfigPath, err)
}
// Устанавливаем правильные права доступа
chmodCmd := exec.Command(rootCmd, "chmod", "644", constants.SystemConfigPath)
if err := chmodCmd.Run(); err != nil {
// Не критично, продолжаем
fmt.Fprintf(os.Stderr, "Предупреждение: не удалось установить права доступа для %s: %v\n", constants.SystemConfigPath, err)
}
return nil
}
func (c *ALRConfig) doCreateDefaultConfig() error {
// Проверяем, существует ли директория для конфига
configDir := filepath.Dir(constants.SystemConfigPath)
if _, err := os.Stat(configDir); os.IsNotExist(err) {
// Пытаемся создать директорию
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("не удалось создать директорию %s: %w", configDir, err)
}
}
// Загружаем дефолтную конфигурацию
defaults := defaultConfigKoanf()
// Копируем все дефолтные значения в системную конфигурацию
c.System.k = defaults
// Сохраняем конфигурацию в файл
if err := c.System.Save(); err != nil {
return fmt.Errorf("не удалось сохранить конфигурацию в %s: %w", constants.SystemConfigPath, err)
}
return nil
}
func (c *ALRConfig) RootCmd() string { return c.cfg.RootCmd }
func (c *ALRConfig) PagerStyle() string { return c.cfg.PagerStyle }
func (c *ALRConfig) AutoPull() bool { return c.cfg.AutoPull }
func (c *ALRConfig) Repos() []types.Repo { return c.cfg.Repos }
func (c *ALRConfig) SetRepos(repos []types.Repo) { c.System.SetRepos(repos) }
func (c *ALRConfig) IgnorePkgUpdates() []string { return c.cfg.IgnorePkgUpdates }
func (c *ALRConfig) LogLevel() string { return c.cfg.LogLevel }
func (c *ALRConfig) UseRootCmd() bool { return c.cfg.UseRootCmd }
func (c *ALRConfig) UpdateSystemOnUpgrade() bool { return c.cfg.UpdateSystemOnUpgrade }
func (c *ALRConfig) GetPaths() *Paths { return c.paths }

View File

@@ -0,0 +1,76 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package config
import (
"strings"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/v2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type EnvConfig struct {
k *koanf.Koanf
}
func NewEnvConfig() *EnvConfig {
return &EnvConfig{
k: koanf.New("."),
}
}
func (c *EnvConfig) koanf() *koanf.Koanf {
return c.k
}
func (c *EnvConfig) Load() error {
allowedKeys := map[string]struct{}{
"ALR_LOG_LEVEL": {},
"ALR_PAGER_STYLE": {},
"ALR_AUTO_PULL": {},
}
err := c.k.Load(env.Provider("ALR_", ".", func(s string) string {
_, ok := allowedKeys[s]
if !ok {
return ""
}
withoutPrefix := strings.TrimPrefix(s, "ALR_")
lowered := strings.ToLower(withoutPrefix)
dotted := strings.ReplaceAll(lowered, "__", ".")
parts := strings.Split(dotted, ".")
for i, part := range parts {
if strings.Contains(part, "_") {
parts[i] = toCamelCase(part)
}
}
return strings.Join(parts, ".")
}), nil)
return err
}
func toCamelCase(s string) string {
parts := strings.Split(s, "_")
for i := 1; i < len(parts); i++ {
if len(parts[i]) > 0 {
parts[i] = cases.Title(language.Und, cases.NoLower).String(parts[i])
}
}
return strings.Join(parts, "")
}

View File

@@ -21,9 +21,10 @@ package config
// Paths contains various paths used by ALR // Paths contains various paths used by ALR
type Paths struct { type Paths struct {
UserConfigPath string SystemConfigPath string
CacheDir string UserConfigPath string
RepoDir string CacheDir string
PkgsDir string RepoDir string
DBPath string PkgsDir string
DBPath string
} }

View File

@@ -0,0 +1,151 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package config
import (
"encoding/json"
"errors"
"fmt"
"os"
ktoml "github.com/knadh/koanf/parsers/toml/v2"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
)
type SystemConfig struct {
k *koanf.Koanf
cfg *types.Config
}
func NewSystemConfig() *SystemConfig {
return &SystemConfig{
k: koanf.New("."),
cfg: &types.Config{},
}
}
func (c *SystemConfig) koanf() *koanf.Koanf {
return c.k
}
func (c *SystemConfig) Load() error {
if _, err := os.Stat(constants.SystemConfigPath); errors.Is(err, os.ErrNotExist) {
return nil
}
if err := c.k.Load(file.Provider(constants.SystemConfigPath), ktoml.Parser()); err != nil {
return err
}
return c.k.Unmarshal("", c.cfg)
}
func (c *SystemConfig) Save() error {
bytes, err := c.k.Marshal(ktoml.Parser())
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
file, err := os.Create(constants.SystemConfigPath)
if err != nil {
return fmt.Errorf("failed to create config file: %w", err)
}
defer func() {
if cerr := file.Close(); cerr != nil && err == nil {
err = cerr
}
}()
if _, err := file.Write(bytes); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
if err := file.Sync(); err != nil {
return fmt.Errorf("failed to sync config: %w", err)
}
return nil
}
func (c *SystemConfig) SetRootCmd(v string) {
err := c.k.Set("rootCmd", v)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetUseRootCmd(v bool) {
err := c.k.Set("useRootCmd", v)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetPagerStyle(v string) {
err := c.k.Set("pagerStyle", v)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetIgnorePkgUpdates(v []string) {
err := c.k.Set("ignorePkgUpdates", v)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetAutoPull(v bool) {
err := c.k.Set("autoPull", v)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetLogLevel(v string) {
err := c.k.Set("logLevel", v)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetRepos(v []types.Repo) {
b, err := json.Marshal(v)
if err != nil {
panic(err)
}
var m []interface{}
err = json.Unmarshal(b, &m)
if err != nil {
panic(err)
}
err = c.k.Set("repo", m)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetUpdateSystemOnUpgrade(v bool) {
err := c.k.Set("updateSystemOnUpgrade", v)
if err != nil {
panic(err)
}
}

View File

@@ -19,6 +19,6 @@ package constants
const ( const (
SystemConfigPath = "/etc/alr/alr.toml" SystemConfigPath = "/etc/alr/alr.toml"
SystemCachePath = "/var/cache/alr" SystemCachePath = "/var/cache/alr"
AlrRunDir = "/var/run/alr" TempDir = "/tmp/alr"
PrivilegedGroup = "wheel" PrivilegedGroup = "wheel"
) )

View File

@@ -21,7 +21,10 @@ package db
import ( import (
"context" "context"
"fmt"
"log/slog" "log/slog"
"os"
"path/filepath"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
_ "modernc.org/sqlite" _ "modernc.org/sqlite"
@@ -54,6 +57,21 @@ func New(config Config) *Database {
func (d *Database) Connect() error { func (d *Database) Connect() error {
dsn := d.config.GetPaths().DBPath dsn := d.config.GetPaths().DBPath
// Проверяем директорию для БД
dbDir := filepath.Dir(dsn)
if _, err := os.Stat(dbDir); err != nil {
if os.IsNotExist(err) {
// Директория не существует - пытаемся создать
if mkErr := os.MkdirAll(dbDir, 0775); mkErr != nil {
// Не смогли создать - вернём ошибку, пользователь должен использовать alr fix
return fmt.Errorf("cache directory does not exist, please run 'alr fix' to create it: %w", mkErr)
}
} else {
return fmt.Errorf("failed to check database directory: %w", err)
}
}
engine, err := xorm.NewEngine("sqlite", dsn) engine, err := xorm.NewEngine("sqlite", dsn)
// engine.SetLogLevel(log.LOG_DEBUG) // engine.SetLogLevel(log.LOG_DEBUG)
// engine.ShowSQL(true) // engine.ShowSQL(true)

663
internal/gen/aur.go Normal file
View File

@@ -0,0 +1,663 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package gen
import (
_ "embed"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"regexp"
"strings"
"text/template"
)
// Встраиваем шаблон для AUR пакетов
//
//go:embed tmpls/aur.tmpl.sh
var aurTmpl string
// AUROptions содержит параметры для генерации шаблона AUR
type AUROptions struct {
Name string // Имя пакета в AUR
Version string // Версия пакета (опционально, если не указана - берется последняя)
CreateDir bool // Создавать ли директорию для пакета и дополнительные файлы
}
// aurAPIResponse представляет структуру ответа от API AUR
type aurAPIResponse struct {
Version int `json:"version"` // Версия API
Type string `json:"type"` // Тип ответа
ResultCount int `json:"resultcount"` // Количество результатов
Results []aurResult `json:"results"` // Массив результатов
Error string `json:"error"` // Сообщение об ошибке (если есть)
}
// aurResult содержит информацию о пакете из AUR
type aurResult struct {
ID int `json:"ID"`
Name string `json:"Name"`
PackageBaseID int `json:"PackageBaseID"`
PackageBase string `json:"PackageBase"`
Version string `json:"Version"`
Description string `json:"Description"`
URL string `json:"URL"`
NumVotes int `json:"NumVotes"`
Popularity float64 `json:"Popularity"`
OutOfDate *int `json:"OutOfDate"`
Maintainer string `json:"Maintainer"`
FirstSubmitted int `json:"FirstSubmitted"`
LastModified int `json:"LastModified"`
URLPath string `json:"URLPath"`
License []string `json:"License"`
Keywords []string `json:"Keywords"`
Depends []string `json:"Depends"`
MakeDepends []string `json:"MakeDepends"`
OptDepends []string `json:"OptDepends"`
CheckDepends []string `json:"CheckDepends"`
Conflicts []string `json:"Conflicts"`
Provides []string `json:"Provides"`
Replaces []string `json:"Replaces"`
// Дополнительные поля для данных из PKGBUILD
Sources []string `json:"-"`
Checksums []string `json:"-"`
BuildFunc string `json:"-"`
PackageFunc string `json:"-"`
PrepareFunc string `json:"-"`
PackageType string `json:"-"` // python, go, rust, cpp, nodejs, bin, git
HasDesktop bool `json:"-"` // Есть ли desktop файлы
HasSystemd bool `json:"-"` // Есть ли systemd сервисы
HasVersion bool `json:"-"` // Есть ли функция version()
HasScripts []string `json:"-"` // Дополнительные скрипты (postinstall, postremove, etc)
HasPatches bool `json:"-"` // Есть ли патчи
Architectures []string `json:"-"` // Поддерживаемые архитектуры
// Автоматически определяемые файлы для install-* команд
BinaryFiles []string `json:"-"` // Исполняемые файлы для install-binary
LicenseFiles []string `json:"-"` // Лицензионные файлы для install-license
ManualFiles []string `json:"-"` // Man страницы для install-manual
DesktopFiles []string `json:"-"` // Desktop файлы для install-desktop
ServiceFiles []string `json:"-"` // Systemd сервисы для install-systemd
CompletionFiles map[string]string `json:"-"` // Файлы автодополнения по типу (bash, zsh, fish)
}
// Вспомогательные методы для шаблона
func (r aurResult) LicenseString() string {
if len(r.License) == 0 {
return "custom:Unknown"
}
// Форматируем лицензии для alr.sh
licenses := make([]string, len(r.License))
for i, l := range r.License {
licenses[i] = fmt.Sprintf("'%s'", l)
}
return strings.Join(licenses, " ")
}
func (r aurResult) DependsString() string {
if len(r.Depends) == 0 {
return ""
}
deps := make([]string, len(r.Depends))
for i, d := range r.Depends {
// Убираем версионные ограничения для простоты
dep := strings.Split(d, ">=")[0]
dep = strings.Split(dep, "<=")[0]
dep = strings.Split(dep, "=")[0]
dep = strings.Split(dep, ">")[0]
dep = strings.Split(dep, "<")[0]
deps[i] = fmt.Sprintf("'%s'", dep)
}
return strings.Join(deps, " ")
}
func (r aurResult) MakeDependsString() string {
if len(r.MakeDepends) == 0 {
return ""
}
deps := make([]string, len(r.MakeDepends))
for i, d := range r.MakeDepends {
// Убираем версионные ограничения для простоты
dep := strings.Split(d, ">=")[0]
dep = strings.Split(dep, "<=")[0]
dep = strings.Split(dep, "=")[0]
dep = strings.Split(dep, ">")[0]
dep = strings.Split(dep, "<")[0]
deps[i] = fmt.Sprintf("'%s'", dep)
}
return strings.Join(deps, " ")
}
func (r aurResult) GitURL() string {
// Формируем URL для клонирования из AUR
return fmt.Sprintf("https://aur.archlinux.org/%s.git", r.PackageBase)
}
func (r aurResult) ArchitecturesString() string {
if len(r.Architectures) == 0 {
return "'all'"
}
archs := make([]string, len(r.Architectures))
for i, arch := range r.Architectures {
archs[i] = fmt.Sprintf("'%s'", arch)
}
return strings.Join(archs, " ")
}
func (r aurResult) OptDependsString() string {
if len(r.OptDepends) == 0 {
return ""
}
optDeps := make([]string, 0, len(r.OptDepends))
for _, dep := range r.OptDepends {
// Форматируем опциональные зависимости для alr.sh
parts := strings.SplitN(dep, ": ", 2)
if len(parts) == 2 {
optDeps = append(optDeps, fmt.Sprintf("'%s: %s'", parts[0], parts[1]))
} else {
optDeps = append(optDeps, fmt.Sprintf("'%s'", dep))
}
}
return strings.Join(optDeps, "\n\t")
}
func (r aurResult) ScriptsString() string {
if len(r.HasScripts) == 0 {
return ""
}
scripts := make([]string, len(r.HasScripts))
for i, script := range r.HasScripts {
scripts[i] = fmt.Sprintf("['%s']='%s.sh'", script, script)
}
return strings.Join(scripts, "\n\t")
}
// GenerateInstallCommands генерирует команды install-* для шаблона
func (r aurResult) GenerateInstallCommands() string {
var commands []string
// install-binary команды
for _, binary := range r.BinaryFiles {
if binary == "./"+r.Name {
commands = append(commands, fmt.Sprintf("\tinstall-binary %s", binary))
} else {
commands = append(commands, fmt.Sprintf("\tinstall-binary %s %s", binary, r.Name))
}
}
// install-license команды
for _, license := range r.LicenseFiles {
commands = append(commands, fmt.Sprintf("\tinstall-license %s %s/LICENSE", license, r.Name))
}
// install-manual команды
for _, manual := range r.ManualFiles {
commands = append(commands, fmt.Sprintf("\tinstall-manual %s", manual))
}
// install-desktop команды
for _, desktop := range r.DesktopFiles {
commands = append(commands, fmt.Sprintf("\tinstall-desktop %s", desktop))
}
// install-systemd команды
for _, service := range r.ServiceFiles {
if strings.Contains(service, "user") {
commands = append(commands, fmt.Sprintf("\tinstall-systemd-user %s", service))
} else {
commands = append(commands, fmt.Sprintf("\tinstall-systemd %s", service))
}
}
// install-completion команды
for shell, file := range r.CompletionFiles {
switch shell {
case "bash":
commands = append(commands, fmt.Sprintf("\tinstall-completion bash %s < %s", r.Name, file))
case "zsh":
commands = append(commands, fmt.Sprintf("\tinstall-completion zsh %s < %s", r.Name, file))
case "fish":
commands = append(commands, fmt.Sprintf("\t%s completion fish | install-completion fish %s", r.Name, r.Name))
}
}
if len(commands) == 0 {
return "\t# TODO: Добавьте команды установки файлов"
}
return strings.Join(commands, "\n")
}
// fetchPKGBUILD загружает PKGBUILD файл для пакета
func fetchPKGBUILD(packageBase string) (string, error) {
// URL для raw PKGBUILD
pkgbuildURL := fmt.Sprintf("https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=%s", packageBase)
res, err := http.Get(pkgbuildURL)
if err != nil {
return "", fmt.Errorf("failed to fetch PKGBUILD: %w", err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
return "", fmt.Errorf("failed to fetch PKGBUILD: status %s", res.Status)
}
data, err := io.ReadAll(res.Body)
if err != nil {
return "", fmt.Errorf("failed to read PKGBUILD: %w", err)
}
return string(data), nil
}
// parseSources извлекает источники из PKGBUILD
func parseSources(pkgbuild string) []string {
var sources []string
// Регулярное выражение для поиска массива source
// Поддерживает как однострочные, так и многострочные определения
sourceRegex := regexp.MustCompile(`(?ms)source=\((.*?)\)`)
matches := sourceRegex.FindStringSubmatch(pkgbuild)
if len(matches) > 1 {
// Извлекаем содержимое массива source
sourceContent := matches[1]
// Разбираем элементы массива
// Учитываем кавычки и переносы строк
elemRegex := regexp.MustCompile(`['"]([^'"]+)['"]`)
elements := elemRegex.FindAllStringSubmatch(sourceContent, -1)
for _, elem := range elements {
if len(elem) > 1 {
source := elem[1]
// Заменяем переменные версии
source = strings.ReplaceAll(source, "$pkgver", "${version}")
source = strings.ReplaceAll(source, "${pkgver}", "${version}")
source = strings.ReplaceAll(source, "$pkgname", "${name}")
source = strings.ReplaceAll(source, "${pkgname}", "${name}")
// Обрабатываем другие переменные (упрощенно)
source = strings.ReplaceAll(source, "$_commit", "${_commit}")
sources = append(sources, source)
}
}
}
// Если источники не найдены в source=(), проверяем source_x86_64 и другие архитектуры
if len(sources) == 0 {
archSourceRegex := regexp.MustCompile(`(?ms)source_(?:x86_64|aarch64)=\((.*?)\)`)
matches = archSourceRegex.FindStringSubmatch(pkgbuild)
if len(matches) > 1 {
sourceContent := matches[1]
elemRegex := regexp.MustCompile(`['"]([^'"]+)['"]`)
elements := elemRegex.FindAllStringSubmatch(sourceContent, -1)
for _, elem := range elements {
if len(elem) > 1 {
source := elem[1]
source = strings.ReplaceAll(source, "$pkgver", "${version}")
source = strings.ReplaceAll(source, "${pkgver}", "${version}")
source = strings.ReplaceAll(source, "$pkgname", "${name}")
source = strings.ReplaceAll(source, "${pkgname}", "${name}")
sources = append(sources, source)
}
}
}
}
return sources
}
// parseChecksums извлекает контрольные суммы из PKGBUILD
func parseChecksums(pkgbuild string) []string {
var checksums []string
// Пробуем разные типы контрольных сумм
for _, hashType := range []string{"sha256sums", "sha512sums", "sha1sums", "md5sums", "b2sums"} {
regex := regexp.MustCompile(fmt.Sprintf(`(?ms)%s=\((.*?)\)`, hashType))
matches := regex.FindStringSubmatch(pkgbuild)
if len(matches) > 1 {
content := matches[1]
elemRegex := regexp.MustCompile(`['"]([^'"]+)['"]`)
elements := elemRegex.FindAllStringSubmatch(content, -1)
for _, elem := range elements {
if len(elem) > 1 {
checksums = append(checksums, elem[1])
}
}
if len(checksums) > 0 {
break // Используем первый найденный тип хешей
}
}
}
return checksums
}
// parseFunctions извлекает функции build(), package() и prepare() из PKGBUILD
func parseFunctions(pkgbuild string) (buildFunc, packageFunc, prepareFunc string) {
// Извлекаем функцию build()
buildRegex := regexp.MustCompile(`(?ms)^build\(\)\s*\{(.*?)^\}`)
if matches := buildRegex.FindStringSubmatch(pkgbuild); len(matches) > 1 {
buildFunc = strings.TrimSpace(matches[1])
}
// Извлекаем функцию package()
packageRegex := regexp.MustCompile(`(?ms)^package\(\)\s*\{(.*?)^\}`)
if matches := packageRegex.FindStringSubmatch(pkgbuild); len(matches) > 1 {
packageFunc = strings.TrimSpace(matches[1])
}
// Извлекаем функцию prepare()
prepareRegex := regexp.MustCompile(`(?ms)^prepare\(\)\s*\{(.*?)^\}`)
if matches := prepareRegex.FindStringSubmatch(pkgbuild); len(matches) > 1 {
prepareFunc = strings.TrimSpace(matches[1])
}
return buildFunc, packageFunc, prepareFunc
}
// detectInstallableFiles анализирует PKGBUILD и определяет файлы для install-* команд
func detectInstallableFiles(pkg *aurResult, pkgbuild string) {
// Инициализируем карту для файлов автодополнения
pkg.CompletionFiles = make(map[string]string)
// Для простоты, добавляем стандартные файлы для типа пакета
switch pkg.PackageType {
case "go":
pkg.BinaryFiles = append(pkg.BinaryFiles, "./"+pkg.Name)
case "rust":
pkg.BinaryFiles = append(pkg.BinaryFiles, "./target/release/"+pkg.Name)
case "cpp", "meson":
pkg.BinaryFiles = append(pkg.BinaryFiles, "./"+pkg.Name) // обычно в корне после сборки
case "bin":
pkg.BinaryFiles = append(pkg.BinaryFiles, "./"+pkg.Name)
default:
if pkg.PackageType != "python" && pkg.PackageType != "nodejs" {
pkg.BinaryFiles = append(pkg.BinaryFiles, "./"+pkg.Name)
}
}
// Ищем лицензионные файлы для install-license с более точными паттернами
licenseRegex := regexp.MustCompile(`(?i)\b(LICENSE|COPYING|COPYRIGHT|LICENCE)(?:\.[a-zA-Z0-9]+)?\b`)
licenseMatches := licenseRegex.FindAllString(pkgbuild, -1)
for _, match := range licenseMatches {
// Фильтруем только реальные файлы лицензий
if strings.Contains(strings.ToLower(match), "license") ||
strings.Contains(strings.ToLower(match), "copying") ||
strings.Contains(strings.ToLower(match), "copyright") {
if !contains(pkg.LicenseFiles, "./"+match) {
pkg.LicenseFiles = append(pkg.LicenseFiles, "./"+match)
}
}
}
// Если не найдены лицензионные файлы, добавляем стандартные
if len(pkg.LicenseFiles) == 0 {
pkg.LicenseFiles = append(pkg.LicenseFiles, "LICENSE")
}
// Ищем man страницы для install-manual с более точными паттернами
manRegex := regexp.MustCompile(`\b\w+\.(?:1|2|3|4|5|6|7|8)(?:\.gz)?\b`)
manMatches := manRegex.FindAllString(pkgbuild, -1)
for _, match := range manMatches {
// Проверяем, что это не переменная или часть кода
if !strings.Contains(match, "$") && !strings.Contains(match, "{") {
if !contains(pkg.ManualFiles, "./"+match) {
pkg.ManualFiles = append(pkg.ManualFiles, "./"+match)
}
}
}
// Ищем desktop файлы для install-desktop
desktopRegex := regexp.MustCompile(`[^/\s]*\.desktop`)
desktopMatches := desktopRegex.FindAllString(pkgbuild, -1)
for _, match := range desktopMatches {
if !contains(pkg.DesktopFiles, "./"+match) {
pkg.DesktopFiles = append(pkg.DesktopFiles, "./"+match)
}
}
// Ищем systemd сервисы для install-systemd
serviceRegex := regexp.MustCompile(`[^/\s]*\.service`)
serviceMatches := serviceRegex.FindAllString(pkgbuild, -1)
for _, match := range serviceMatches {
if !contains(pkg.ServiceFiles, "./"+match) {
pkg.ServiceFiles = append(pkg.ServiceFiles, "./"+match)
}
}
// Ищем файлы автодополнения
completionPatterns := map[string]string{
"bash": `completions?/.*\.bash|bash-completion`,
"zsh": `completions?/.*\.zsh|zsh.*completion`,
"fish": `completions?/.*\.fish|fish.*completion`,
}
for shell, pattern := range completionPatterns {
regex := regexp.MustCompile(fmt.Sprintf(`(?i)%s`, pattern))
matches := regex.FindAllString(pkgbuild, -1)
if len(matches) > 0 {
pkg.CompletionFiles[shell] = matches[0]
}
}
}
// contains проверяет, содержит ли слайс строк указанную строку
func contains(slice []string, item string) bool {
for _, s := range slice {
if s == item {
return true
}
}
return false
}
// detectPackageType определяет тип пакета на основе имени, зависимостей и источников
func detectPackageType(pkg *aurResult, pkgbuild string) {
name := strings.ToLower(pkg.Name)
// Определяем тип на основе имени пакета
switch {
case strings.HasPrefix(name, "python") || strings.HasPrefix(name, "python3-"):
pkg.PackageType = "python"
case strings.Contains(name, "nodejs") || strings.Contains(name, "node-"):
pkg.PackageType = "nodejs"
case strings.HasSuffix(name, "-bin"):
pkg.PackageType = "bin"
case strings.HasSuffix(name, "-git"):
pkg.PackageType = "git"
pkg.HasVersion = true // Git пакеты обычно имеют функцию version()
case strings.Contains(name, "rust") || hasRustSources(pkg.Sources):
pkg.PackageType = "rust"
case strings.Contains(name, "go-") || hasGoSources(pkg.Sources):
pkg.PackageType = "go"
case strings.Contains(name, "-rust") || strings.Contains(name, "paru") || strings.Contains(name, "cargo-"):
pkg.PackageType = "rust"
default:
// Определяем по зависимостям сборки
for _, dep := range pkg.MakeDepends {
depLower := strings.ToLower(dep)
switch {
case strings.Contains(depLower, "meson") || strings.Contains(depLower, "ninja"):
pkg.PackageType = "meson"
case strings.Contains(depLower, "cmake") || strings.Contains(depLower, "gcc") || strings.Contains(depLower, "clang"):
pkg.PackageType = "cpp"
case strings.Contains(depLower, "python"):
pkg.PackageType = "python"
case strings.Contains(depLower, "go"):
pkg.PackageType = "go"
case strings.Contains(depLower, "rust") || strings.Contains(depLower, "cargo"):
pkg.PackageType = "rust"
case strings.Contains(depLower, "npm") || strings.Contains(depLower, "nodejs"):
pkg.PackageType = "nodejs"
}
if pkg.PackageType != "" {
break
}
}
}
// Определяем архитектуры на основе типа пакета
if pkg.PackageType == "bin" {
pkg.Architectures = []string{"amd64"} // Бинарные пакеты обычно специфичны для архитектуры
} else {
pkg.Architectures = []string{"all"} // Исходный код собирается для любой архитектуры
}
// Определяем наличие desktop файлов
pkg.HasDesktop = strings.Contains(pkgbuild, ".desktop") ||
strings.Contains(pkgbuild, "install-desktop") ||
strings.Contains(pkgbuild, "xdg-desktop")
// Определяем наличие systemd сервисов
pkg.HasSystemd = strings.Contains(pkgbuild, ".service") ||
strings.Contains(pkgbuild, "systemctl") ||
strings.Contains(pkgbuild, "install-systemd")
// Определяем наличие функции version() для -git пакетов
pkg.HasVersion = strings.Contains(pkgbuild, "pkgver()") ||
(strings.HasSuffix(name, "-git") && strings.Contains(pkgbuild, "git describe"))
// Определяем наличие патчей
pkg.HasPatches = strings.Contains(pkgbuild, "patch ") ||
strings.Contains(pkgbuild, ".patch") ||
strings.Contains(pkgbuild, ".diff")
// Определяем дополнительные скрипты
if strings.Contains(pkgbuild, "post_install") {
pkg.HasScripts = append(pkg.HasScripts, "postinstall")
}
if strings.Contains(pkgbuild, "pre_remove") || strings.Contains(pkgbuild, "post_remove") {
pkg.HasScripts = append(pkg.HasScripts, "postremove")
}
}
// hasRustSources проверяет, содержат ли источники Rust проекты
func hasRustSources(sources []string) bool {
for _, src := range sources {
if strings.Contains(src, "crates.io") || strings.Contains(src, "Cargo.toml") {
return true
}
}
return false
}
// hasGoSources проверяет, содержат ли источники Go проекты
func hasGoSources(sources []string) bool {
for _, src := range sources {
if strings.Contains(src, "github.com") && strings.Contains(src, "/go") {
return true
}
}
return false
}
// AUR генерирует шаблон alr.sh на основе пакета из AUR
func AUR(w io.Writer, opts AUROptions) error {
// Создаем шаблон с функциями
tmpl, err := template.New("aur").
Funcs(funcs).
Parse(aurTmpl)
if err != nil {
return err
}
// Формируем URL запроса к AUR API
apiURL := "https://aur.archlinux.org/rpc/v5/info"
params := url.Values{}
params.Add("arg[]", opts.Name)
fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode())
// Выполняем запрос к AUR API
res, err := http.Get(fullURL)
if err != nil {
return fmt.Errorf("failed to fetch AUR package info: %w", err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
return fmt.Errorf("AUR API returned status: %s", res.Status)
}
// Декодируем ответ
var resp aurAPIResponse
err = json.NewDecoder(res.Body).Decode(&resp)
if err != nil {
return fmt.Errorf("failed to decode AUR response: %w", err)
}
// Проверяем наличие ошибки в ответе
if resp.Error != "" {
return fmt.Errorf("AUR API error: %s", resp.Error)
}
// Проверяем, что пакет найден
if resp.ResultCount == 0 {
return fmt.Errorf("package '%s' not found in AUR", opts.Name)
}
// Берем первый результат
pkg := resp.Results[0]
// Если указана версия, проверяем соответствие
if opts.Version != "" && pkg.Version != opts.Version {
// Предупреждаем, но продолжаем с актуальной версией из AUR
fmt.Fprintf(w, "# WARNING: Requested version %s, but AUR has %s\n", opts.Version, pkg.Version)
}
// Загружаем PKGBUILD для получения источников
pkgbuild, err := fetchPKGBUILD(pkg.PackageBase)
if err != nil {
// Если не удалось загрузить PKGBUILD, используем fallback на AUR репозиторий
fmt.Fprintf(w, "# WARNING: Could not fetch PKGBUILD: %v\n", err)
fmt.Fprintf(w, "# Using AUR repository as source\n")
pkg.Sources = []string{fmt.Sprintf("%s::git+%s", pkg.Name, pkg.GitURL())}
pkg.Checksums = []string{"SKIP"}
} else {
// Извлекаем источники из PKGBUILD
pkg.Sources = parseSources(pkgbuild)
pkg.Checksums = parseChecksums(pkgbuild)
pkg.BuildFunc, pkg.PackageFunc, pkg.PrepareFunc = parseFunctions(pkgbuild)
// Определяем тип пакета
detectPackageType(&pkg, pkgbuild)
// Определяем файлы для install-* команд
detectInstallableFiles(&pkg, pkgbuild)
// Если источники не найдены, используем fallback
if len(pkg.Sources) == 0 {
fmt.Fprintf(w, "# WARNING: No sources found in PKGBUILD\n")
fmt.Fprintf(w, "# Using AUR repository as source\n")
pkg.Sources = []string{fmt.Sprintf("%s::git+%s", pkg.Name, pkg.GitURL())}
pkg.Checksums = []string{"SKIP"}
}
}
// Выполняем шаблон
return tmpl.Execute(w, pkg)
}

View File

@@ -0,0 +1,133 @@
# This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
# It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
#
# ALR - Any Linux Repository
# Copyright (C) 2025 The ALR Authors
#
# 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 <http://www.gnu.org/licenses/>.
# Generated from AUR package: {{.Name}}
# Package type: {{.PackageType}}
# AUR votes: {{.NumVotes}} | Popularity: {{printf "%.2f" .Popularity}}
# Original maintainer: {{.Maintainer}}
# Adapted for ALR by automation
name='{{.Name}}'
version='{{.Version}}'
release='1'
desc='{{.Description}}'
{{if ne .Description ""}}desc_ru='{{.Description}}'{{end}}
homepage='{{.URL}}'
maintainer="Евгений Храмов <xpamych@yandex.ru> (imported from AUR)"
{{if ne .Description ""}}maintainer_ru="Евгений Храмов <xpamych@yandex.ru> (импортирован из AUR)"{{end}}
architectures=({{.ArchitecturesString}})
license=({{.LicenseString}})
{{if .Provides}}provides=({{range .Provides}}'{{.}}' {{end}}){{end}}
{{if .Conflicts}}conflicts=({{range .Conflicts}}'{{.}}' {{end}}){{end}}
{{if .Replaces}}replaces=({{range .Replaces}}'{{.}}' {{end}}){{end}}
# Базовые зависимости
{{if .DependsString}}deps=({{.DependsString}}){{else}}deps=(){{end}}
{{if .MakeDependsString}}build_deps=({{.MakeDependsString}}){{else}}build_deps=(){{end}}
# Зависимости для конкретных дистрибутивов (адаптируйте под нужды пакета)
{{if .DependsString}}deps_arch=({{.DependsString}})
deps_debian=({{.DependsString}})
deps_altlinux=({{.DependsString}})
deps_alpine=({{.DependsString}}){{end}}
{{if and .MakeDependsString (ne .PackageType "bin")}}# Зависимости сборки для конкретных дистрибутивов
build_deps_arch=({{.MakeDependsString}})
build_deps_debian=({{.MakeDependsString}})
build_deps_altlinux=({{.MakeDependsString}})
build_deps_alpine=({{.MakeDependsString}}){{end}}
{{if .OptDependsString}}# Опциональные зависимости
opt_deps=(
{{.OptDependsString}}
){{end}}
# Источники из PKGBUILD
sources=({{range .Sources}}"{{.}}" {{end}})
checksums=({{range .Checksums}}'{{.}}' {{end}})
{{if .HasVersion}}# Функция версии для Git-пакетов
version() {
cd "$srcdir/{{.Name}}"
git-version
}
{{end}}
{{if .ScriptsString}}# Дополнительные скрипты
scripts=(
{{.ScriptsString}}
){{end}}
{{if or .PrepareFunc .HasPatches}}prepare() {
cd "$srcdir"{{if .PrepareFunc}}
# Из PKGBUILD:
{{.PrepareFunc}}{{else}}
# Применение патчей и подготовка исходников
# Раскомментируйте и адаптируйте при необходимости:
# patch -p1 < "${scriptdir}/fix.patch"{{end}}
}{{else}}# prepare() {
# cd "$srcdir"
# # Применение патчей и подготовка исходников при необходимости
# # patch -p1 < "${scriptdir}/fix.patch"
# }{{end}}
{{if ne .PackageType "bin"}}build() {
cd "$srcdir"{{if .BuildFunc}}
# Из PKGBUILD:
{{.BuildFunc}}{{else}}
# TODO: Адаптируйте команды сборки под конкретный проект ({{.PackageType}})
{{if eq .PackageType "meson"}}# Для Meson проектов:
meson setup build --prefix=/usr --buildtype=release
ninja -C build -j $(nproc){{else if eq .PackageType "cpp"}}# Для C/C++ проектов:
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
make -j$(nproc){{else if eq .PackageType "go"}}# Для Go проектов:
go build -buildmode=pie -trimpath -ldflags "-s -w" -o {{.Name}}{{else if eq .PackageType "python"}}# Для Python проектов:
python -m build --wheel --no-isolation{{else if eq .PackageType "nodejs"}}# Для Node.js проектов:
npm ci --production
npm run build{{else if eq .PackageType "rust"}}# Для Rust проектов:
cargo build --release --locked{{else if eq .PackageType "git"}}# Для Git проектов (обычно исходный код):
make -j$(nproc){{else}}# Стандартная сборка:
make -j$(nproc){{end}}{{end}}
}{{else}}# Бинарный пакет - сборка не требуется{{end}}
package() {
cd "$srcdir"{{if .PackageFunc}}
# Из PKGBUILD (адаптировано для ALR):
{{.PackageFunc}}
# Автоматически сгенерированные команды установки:
{{.GenerateInstallCommands}}{{else}}
# TODO: Адаптируйте установку файлов под конкретный проект {{.Name}}
{{if eq .PackageType "meson"}}# Для Meson проектов:
meson install -C build --destdir="$pkgdir"{{else if eq .PackageType "cpp"}}# Для C/C++ проектов:
cd build
make DESTDIR="$pkgdir" install{{else if eq .PackageType "go"}}# Для Go проектов:
# Исполняемый файл уже собран в корне{{else if eq .PackageType "python"}}# Для Python проектов:
pip install --root="$pkgdir/" . --no-deps --disable-pip-version-check{{else if eq .PackageType "nodejs"}}# Для Node.js проектов:
npm install -g --prefix="$pkgdir/usr" .{{else if eq .PackageType "rust"}}# Для Rust проектов:
# Исполняемый файл в target/release/{{else if eq .PackageType "bin"}}# Бинарный пакет:
# Файлы уже распакованы{{else}}# Стандартная установка:
make DESTDIR="$pkgdir" install{{end}}
# Автоматически сгенерированные команды установки:
{{.GenerateInstallCommands}}{{end}}
}

View File

@@ -16,14 +16,30 @@
package manager package manager
import "os/exec" import (
"os"
"os/exec"
)
type CommonPackageManager struct { type CommonPackageManager struct {
noConfirmArg string noConfirmArg string
} }
func (m *CommonPackageManager) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd { func (m *CommonPackageManager) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
cmd := exec.Command(mgrCmd) var cmd *exec.Cmd
// Проверяем, нужно ли повышение привилегий
isRoot := os.Geteuid() == 0
isCI := os.Getenv("CI") == "true"
if !isRoot && !isCI {
// Если не root и не в CI, используем sudo
cmd = exec.Command("sudo", mgrCmd)
} else {
// Если root или в CI, запускаем напрямую
cmd = exec.Command(mgrCmd)
}
cmd.Args = append(cmd.Args, opts.Args...) cmd.Args = append(cmd.Args, opts.Args...)
cmd.Args = append(cmd.Args, args...) cmd.Args = append(cmd.Args, args...)

View File

@@ -22,6 +22,7 @@ package overrides
import ( import (
"fmt" "fmt"
"regexp" "regexp"
"strconv"
"strings" "strings"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
@@ -182,3 +183,18 @@ func ReleasePlatformSpecific(release int, info *distro.OSRelease) string {
return fmt.Sprintf("%d", release) return fmt.Sprintf("%d", release)
} }
func ParseReleasePlatformSpecific(s string, info *distro.OSRelease) (int, error) {
if info.ID == "altlinux" {
if strings.HasPrefix(s, "alt") {
return strconv.Atoi(s[3:])
}
}
if info.ID == "fedora" || slices.Contains(info.Like, "fedora") {
parts := strings.SplitN(s, ".", 2)
return strconv.Atoi(parts[0])
}
return strconv.Atoi(s)
}

View File

@@ -233,5 +233,8 @@ func TestReleasePlatformSpecific(t *testing.T) {
}, },
} { } {
assert.Equal(t, tc.expected, overrides.ReleasePlatformSpecific(1, tc.info)) assert.Equal(t, tc.expected, overrides.ReleasePlatformSpecific(1, tc.info))
release, err := overrides.ParseReleasePlatformSpecific(tc.expected, tc.info)
assert.NoError(t, err)
assert.Equal(t, 1, release)
} }
} }

View File

@@ -21,44 +21,65 @@ package repos
import ( import (
"context" "context"
"fmt"
"strings"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
) )
func (rs *Repos) FindPkgs(ctx context.Context, pkgs []string) (map[string][]alrsh.Package, []string, error) { func (rs *Repos) FindPkgs(ctx context.Context, pkgs []string) (map[string][]alrsh.Package, []string, error) {
found := map[string][]alrsh.Package{} found := make(map[string][]alrsh.Package)
notFound := []string(nil) var notFound []string
for _, pkgName := range pkgs { for _, pkgName := range pkgs {
if pkgName == "" { if pkgName == "" {
continue continue
} }
result, err := rs.db.GetPkgs(ctx, "json_array_contains(provides, ?)", pkgName) var result []alrsh.Package
if err != nil { var err error
return nil, nil, err
}
added := 0 switch {
for _, pkg := range result { case strings.Contains(pkgName, "/"):
added++ // repo/pkg
found[pkgName] = append(found[pkgName], pkg) parts := strings.SplitN(pkgName, "/", 2)
} repo := parts[0]
name := parts[1]
result, err = rs.db.GetPkgs(ctx, "name = ? AND repository = ?", name, repo)
if added == 0 { case strings.Contains(pkgName, "+alr-"):
result, err := rs.db.GetPkgs(ctx, "name LIKE ?", pkgName) // pkg+alr-repo
parts := strings.SplitN(pkgName, "+alr-", 2)
name := parts[0]
repo := parts[1]
result, err = rs.db.GetPkgs(ctx, "name = ? AND repository = ?", name, repo)
default:
result, err = rs.db.GetPkgs(ctx, "json_array_contains(provides, ?)", pkgName)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, fmt.Errorf("FindPkgs: get by provides: %w", err)
} }
for _, pkg := range result { if len(result) == 0 {
added++ result, err = rs.db.GetPkgs(ctx, "basepkg_name = ?", pkgName)
found[pkgName] = append(found[pkgName], pkg) if err != nil {
return nil, nil, fmt.Errorf("FindPkgs: get by basepkg_name: %w", err)
}
}
if len(result) == 0 {
result, err = rs.db.GetPkgs(ctx, "name LIKE ?", pkgName)
} }
} }
if added == 0 { if err != nil {
return nil, nil, fmt.Errorf("FindPkgs: lookup for %q failed: %w", pkgName, err)
}
if len(result) == 0 {
notFound = append(notFound, pkgName) notFound = append(notFound, pkgName)
} else {
found[pkgName] = result
} }
} }

View File

@@ -31,7 +31,6 @@ import (
"strings" "strings"
"github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
gitConfig "github.com/go-git/go-git/v5/config" gitConfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing" "github.com/go-git/go-git/v5/plumbing"
@@ -69,159 +68,240 @@ func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error {
} }
for _, repo := range repos { for _, repo := range repos {
repoURL, err := url.Parse(repo.URL) err := rs.pullRepo(ctx, &repo, false)
if err != nil { if err != nil {
return err return err
} }
}
slog.Info(gotext.Get("Pulling repository"), "name", repo.Name) return nil
repoDir := filepath.Join(rs.cfg.GetPaths().RepoDir, repo.Name) }
var repoFS billy.Filesystem func (rs *Repos) PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) error {
gitDir := filepath.Join(repoDir, ".git") err := rs.pullRepo(ctx, repo, true)
// Only pull repos that contain valid git repos if err != nil {
if fi, err := os.Stat(gitDir); err == nil && fi.IsDir() { return err
r, err := git.PlainOpen(repoDir) }
if err != nil {
return err
}
err = r.FetchContext(ctx, &git.FetchOptions{ return nil
Progress: os.Stderr, }
Force: true,
})
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return err
}
w, err := r.Worktree() func (rs *Repos) pullRepo(ctx context.Context, repo *types.Repo, updateRepoFromToml bool) error {
if err != nil { urls := []string{repo.URL}
return err urls = append(urls, repo.Mirrors...)
}
old, err := r.Head() var lastErr error
if err != nil {
return err
}
revHash, err := resolveHash(r, repo.Ref) for i, repoURL := range urls {
if err != nil { if i > 0 {
return fmt.Errorf("error resolving hash: %w", err) slog.Info(gotext.Get("Trying mirror"), "repo", repo.Name, "mirror", repoURL)
}
if old.Hash() == *revHash {
slog.Info(gotext.Get("Repository up to date"), "name", repo.Name)
}
err = w.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(revHash.String()),
Force: true,
})
if err != nil {
return err
}
repoFS = w.Filesystem
new, err := r.Head()
if err != nil {
return err
}
// If the DB was not present at startup, that means it's
// empty. In this case, we need to update the DB fully
// rather than just incrementally.
if rs.db.IsEmpty() {
err = rs.processRepoFull(ctx, repo, repoDir)
if err != nil {
return err
}
} else {
err = rs.processRepoChanges(ctx, repo, r, w, old, new)
if err != nil {
return err
}
}
} else {
err = os.RemoveAll(repoDir)
if err != nil {
return err
}
err = os.MkdirAll(repoDir, 0o755)
if err != nil {
return err
}
r, err := git.PlainInit(repoDir, false)
if err != nil {
return err
}
_, err = r.CreateRemote(&gitConfig.RemoteConfig{
Name: git.DefaultRemoteName,
URLs: []string{repoURL.String()},
})
if err != nil {
return err
}
err = r.FetchContext(ctx, &git.FetchOptions{
Progress: os.Stderr,
Force: true,
})
if err != nil {
return err
}
w, err := r.Worktree()
if err != nil {
return err
}
revHash, err := resolveHash(r, repo.Ref)
if err != nil {
return fmt.Errorf("error resolving hash: %w", err)
}
err = w.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(revHash.String()),
Force: true,
})
if err != nil {
return err
}
err = rs.processRepoFull(ctx, repo, repoDir)
if err != nil {
return err
}
repoFS = osfs.New(repoDir)
} }
fl, err := repoFS.Open("alr-repo.toml") err := rs.pullRepoFromURL(ctx, repoURL, repo, updateRepoFromToml)
if err != nil { if err != nil {
slog.Warn(gotext.Get("Git repository does not appear to be a valid ALR repo"), "repo", repo.Name) lastErr = err
slog.Warn(gotext.Get("Failed to pull from URL"), "repo", repo.Name, "url", repoURL, "error", err)
continue continue
} }
var repoCfg types.RepoConfig // Success
err = toml.NewDecoder(fl).Decode(&repoCfg) return nil
}
return fmt.Errorf("failed to pull repository %s from any URL: %w", repo.Name, lastErr)
}
func readGitRepo(repoDir, repoUrl string) (*git.Repository, bool, error) {
gitDir := filepath.Join(repoDir, ".git")
if fi, err := os.Stat(gitDir); err == nil && fi.IsDir() {
r, err := git.PlainOpen(repoDir)
if err == nil {
err = updateRemoteURL(r, repoUrl)
if err == nil {
_, err := r.Head()
if err == nil {
return r, false, nil
}
if errors.Is(err, plumbing.ErrReferenceNotFound) {
return r, true, nil
}
slog.Debug("error getting HEAD, reinitializing...", "err", err)
}
}
slog.Debug("error while reading repo, reinitializing...", "err", err)
}
if err := os.RemoveAll(repoDir); err != nil {
return nil, false, fmt.Errorf("failed to remove repo directory: %w", err)
}
if err := os.MkdirAll(repoDir, 0o755); err != nil {
return nil, false, fmt.Errorf("failed to create repo directory: %w", err)
}
r, err := git.PlainInit(repoDir, false)
if err != nil {
return nil, false, fmt.Errorf("failed to initialize git repo: %w", err)
}
_, err = r.CreateRemote(&gitConfig.RemoteConfig{
Name: git.DefaultRemoteName,
URLs: []string{repoUrl},
})
if err != nil {
return nil, false, err
}
return r, true, nil
}
func (rs *Repos) pullRepoFromURL(ctx context.Context, rawRepoUrl string, repo *types.Repo, update bool) error {
repoURL, err := url.Parse(rawRepoUrl)
if err != nil {
return fmt.Errorf("invalid URL %s: %w", rawRepoUrl, err)
}
slog.Info(gotext.Get("Pulling repository"), "name", repo.Name)
repoDir := filepath.Join(rs.cfg.GetPaths().RepoDir, repo.Name)
var repoFS billy.Filesystem
r, freshGit, err := readGitRepo(repoDir, repoURL.String())
if err != nil {
return fmt.Errorf("failed to open repo")
}
err = r.FetchContext(ctx, &git.FetchOptions{
Progress: os.Stderr,
Force: true,
})
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return err
}
var old *plumbing.Reference
w, err := r.Worktree()
if err != nil {
return err
}
revHash, err := resolveHash(r, repo.Ref)
if err != nil {
return fmt.Errorf("error resolving hash: %w", err)
}
if !freshGit {
old, err = r.Head()
if err != nil { if err != nil {
return err return err
} }
fl.Close()
// If the version doesn't have a "v" prefix, it's not a standard version. if old.Hash() == *revHash {
// It may be "unknown" or a git version, but either way, there's no way slog.Info(gotext.Get("Repository up to date"), "name", repo.Name)
// to compare it to the repo version, so only compare versions with the "v".
if strings.HasPrefix(config.Version, "v") {
if vercmp.Compare(config.Version, repoCfg.Repo.MinVersion) == -1 {
slog.Warn(gotext.Get("ALR repo's minimum ALR version is greater than the current version. Try updating ALR if something doesn't work."), "repo", repo.Name)
}
} }
} }
err = w.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(revHash.String()),
Force: true,
})
if err != nil {
return err
}
repoFS = w.Filesystem
new, err := r.Head()
if err != nil {
return err
}
// If the DB was not present at startup, that means it's
// empty. In this case, we need to update the DB fully
// rather than just incrementally.
if rs.db.IsEmpty() || freshGit {
err = rs.processRepoFull(ctx, *repo, repoDir)
if err != nil {
return err
}
} else {
err = rs.processRepoChanges(ctx, *repo, r, w, old, new)
if err != nil {
return err
}
}
fl, err := repoFS.Open("alr-repo.toml")
if err != nil {
slog.Warn(gotext.Get("Git repository does not appear to be a valid ALR repo"), "repo", repo.Name)
return nil
}
var repoCfg types.RepoConfig
err = toml.NewDecoder(fl).Decode(&repoCfg)
if err != nil {
return err
}
fl.Close()
// If the version doesn't have a "v" prefix, it's not a standard version.
// It may be "unknown" or a git version, but either way, there's no way
// to compare it to the repo version, so only compare versions with the "v".
if strings.HasPrefix(config.Version, "v") {
if vercmp.Compare(config.Version, repoCfg.Repo.MinVersion) == -1 {
slog.Warn(gotext.Get("ALR repo's minimum ALR version is greater than the current version. Try updating ALR if something doesn't work."), "repo", repo.Name)
}
}
if update {
if repoCfg.Repo.URL != "" {
repo.URL = repoCfg.Repo.URL
}
if repoCfg.Repo.Ref != "" {
repo.Ref = repoCfg.Repo.Ref
}
if len(repoCfg.Repo.Mirrors) > 0 {
repo.Mirrors = repoCfg.Repo.Mirrors
}
}
return nil
}
func updateRemoteURL(r *git.Repository, newURL string) error {
cfg, err := r.Config()
if err != nil {
return err
}
remote, ok := cfg.Remotes[git.DefaultRemoteName]
if !ok || len(remote.URLs) == 0 {
return fmt.Errorf("no remote '%s' found", git.DefaultRemoteName)
}
currentURL := remote.URLs[0]
if currentURL == newURL {
return nil
}
slog.Debug("Updating remote URL", "old", currentURL, "new", newURL)
err = r.DeleteRemote(git.DefaultRemoteName)
if err != nil {
return fmt.Errorf("failed to delete old remote: %w", err)
}
_, err = r.CreateRemote(&gitConfig.RemoteConfig{
Name: git.DefaultRemoteName,
URLs: []string{newURL},
})
if err != nil {
return fmt.Errorf("failed to create new remote: %w", err)
}
return nil return nil
} }

View File

@@ -26,7 +26,6 @@ import (
"testing" "testing"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db" database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/repos" "gitea.plemya-x.ru/Plemya-x/ALR/internal/repos"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
@@ -35,7 +34,7 @@ import (
type TestEnv struct { type TestEnv struct {
Ctx context.Context Ctx context.Context
Cfg *TestALRConfig Cfg *TestALRConfig
Db *db.Database Db *database.Database
} }
type TestALRConfig struct { type TestALRConfig struct {

View File

@@ -23,7 +23,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log/slog"
"reflect" "reflect"
"strings" "strings"
@@ -75,75 +74,99 @@ func New(info *distro.OSRelease, runner *interp.Runner) *Decoder {
// DecodeVar decodes a variable to val using reflection. // DecodeVar decodes a variable to val using reflection.
// Structs should use the "sh" struct tag. // Structs should use the "sh" struct tag.
func (d *Decoder) DecodeVar(name string, val any) error { func (d *Decoder) DecodeVar(name string, val any) error {
variable := d.getVar(name) origType := reflect.TypeOf(val).Elem()
if variable == nil { isOverridableField := strings.Contains(origType.String(), "OverridableField[")
return VarNotFoundError{name}
}
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ if !isOverridableField {
WeaklyTypedInput: true, variable := d.getVarNoOverrides(name)
DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) { if variable == nil {
if strings.Contains(to.Type().String(), "alrsh.OverridableField") { return VarNotFoundError{name}
if to.Kind() != reflect.Ptr && to.CanAddr() { }
to = to.Addr()
}
names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name)) dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
if err != nil { WeaklyTypedInput: true,
return nil, err Result: val, // передаем указатель на новое значение
} TagName: "sh",
DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) {
isNotSet := true if from.Kind() == reflect.Slice && to.Kind() == reflect.String {
s, ok := from.Interface().([]string)
setMethod := to.MethodByName("Set") if ok && len(s) == 1 {
setResolvedMethod := to.MethodByName("SetResolved") return s[0], nil
for _, varName := range names {
val := d.getVarNoOverrides(varName)
if val == nil {
continue
} }
t := setMethod.Type().In(1)
newVal := from
if !newVal.Type().AssignableTo(t) {
newVal = reflect.New(t)
err = d.DecodeVar(name, newVal.Interface())
if err != nil {
return nil, err
}
newVal = newVal.Elem()
}
if isNotSet {
setResolvedMethod.Call([]reflect.Value{newVal})
}
override := strings.TrimPrefix(strings.TrimPrefix(varName, name), "_")
setMethod.Call([]reflect.Value{reflect.ValueOf(override), newVal})
} }
return from.Interface(), nil
}),
})
if err != nil {
return err
}
return to, nil switch variable.Kind {
case expand.Indexed:
return dec.Decode(variable.List)
case expand.Associative:
return dec.Decode(variable.Map)
default:
return dec.Decode(variable.Str)
}
} else {
vars := d.getVarsByPrefix(name)
if len(vars) == 0 {
return VarNotFoundError{name}
}
reflectVal := reflect.ValueOf(val)
overridableVal := reflect.ValueOf(val).Elem()
dataField := overridableVal.FieldByName("data")
if !dataField.IsValid() {
return fmt.Errorf("data field not found in OverridableField")
}
mapType := dataField.Type() // map[string]T
elemType := mapType.Elem() // T
var overridablePtr reflect.Value
if reflectVal.Kind() == reflect.Ptr {
overridablePtr = reflectVal
} else {
if !reflectVal.CanAddr() {
return fmt.Errorf("OverridableField value is not addressable")
} }
return from.Interface(), nil overridablePtr = reflectVal.Addr()
}), }
Result: val,
TagName: "sh",
})
if err != nil {
slog.Warn("err", "err", err)
return err
}
switch variable.Kind { setValue := overridablePtr.MethodByName("Set")
case expand.Indexed: if !setValue.IsValid() {
return dec.Decode(variable.List) return fmt.Errorf("method Set not found on OverridableField")
case expand.Associative: }
return dec.Decode(variable.Map)
default: for _, v := range vars {
return dec.Decode(variable.Str) varName := v.Name
key := strings.TrimPrefix(strings.TrimPrefix(varName, name), "_")
newVal := reflect.New(elemType)
if err := d.DecodeVar(varName, newVal.Interface()); err != nil {
return err
}
keyValue := reflect.ValueOf(key)
setValue.Call([]reflect.Value{keyValue, newVal.Elem()})
}
resolveValue := overridablePtr.MethodByName("Resolve")
if !resolveValue.IsValid() {
return fmt.Errorf("method Resolve not found on OverridableField")
}
names, err := overrides.Resolve(d.info, overrides.DefaultOpts)
if err != nil {
return err
}
resolveValue.Call([]reflect.Value{reflect.ValueOf(names)})
return nil
} }
} }
@@ -284,23 +307,6 @@ func (d *Decoder) getFunc(name string) *syntax.Stmt {
return nil return nil
} }
// getVar gets a variable based on its name, taking into account
// override variables and nameref variables.
func (d *Decoder) getVar(name string) *expand.Variable {
names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name))
if err != nil {
return nil
}
for _, varName := range names {
res := d.getVarNoOverrides(varName)
if res != nil {
return res
}
}
return nil
}
func (d *Decoder) getVarNoOverrides(name string) *expand.Variable { func (d *Decoder) getVarNoOverrides(name string) *expand.Variable {
val, ok := d.Runner.Vars[name] val, ok := d.Runner.Vars[name]
if ok { if ok {
@@ -318,6 +324,32 @@ func (d *Decoder) getVarNoOverrides(name string) *expand.Variable {
return nil return nil
} }
type vars struct {
Name string
Value *expand.Variable
}
func (d *Decoder) getVarsByPrefix(prefix string) []*vars {
result := make([]*vars, 0)
for name, val := range d.Runner.Vars {
if !strings.HasPrefix(name, prefix) {
continue
}
switch prefix {
case "auto_req":
if strings.HasPrefix(name, "auto_req_skiplist") {
continue
}
case "auto_prov":
if strings.HasPrefix(name, "auto_prov_skiplist") {
continue
}
}
result = append(result, &vars{name, &val})
}
return result
}
func IsTruthy(value string) bool { func IsTruthy(value string) bool {
value = strings.ToLower(strings.TrimSpace(value)) value = strings.ToLower(strings.TrimSpace(value))
return value == "true" || value == "yes" || value == "1" return value == "true" || value == "yes" || value == "1"

View File

@@ -32,24 +32,25 @@ import (
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder" "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
) )
type BuildVars struct { type BuildVars struct {
Name string `sh:"name,required"` Name string `sh:"name,required"`
Version string `sh:"version,required"` Version string `sh:"version,required"`
Release int `sh:"release,required"` Release int `sh:"release,required"`
Epoch uint `sh:"epoch"` Epoch uint `sh:"epoch"`
Description string `sh:"desc"` Description alrsh.OverridableField[string] `sh:"desc"`
Homepage string `sh:"homepage"` Homepage string `sh:"homepage"`
Maintainer string `sh:"maintainer"` Maintainer string `sh:"maintainer"`
Architectures []string `sh:"architectures"` Architectures []string `sh:"architectures"`
Licenses []string `sh:"license"` Licenses []string `sh:"license"`
Provides []string `sh:"provides"` Provides []string `sh:"provides"`
Conflicts []string `sh:"conflicts"` Conflicts []string `sh:"conflicts"`
Depends []string `sh:"deps"` Depends []string `sh:"deps"`
BuildDepends []string `sh:"build_deps"` BuildDepends alrsh.OverridableField[[]string] `sh:"build_deps"`
Replaces []string `sh:"replaces"` Replaces alrsh.OverridableField[[]string] `sh:"replaces"`
} }
const testScript = ` const testScript = `
@@ -113,22 +114,34 @@ func TestDecodeVars(t *testing.T) {
} }
expected := BuildVars{ expected := BuildVars{
Name: "test", Name: "test",
Version: "0.0.1", Version: "0.0.1",
Release: 1, Release: 1,
Epoch: 2, Epoch: 2,
Description: "Test package", Description: alrsh.OverridableFromMap(map[string]string{
"": "Test package",
}),
Homepage: "https://gitea.plemya-x.ru/xpamych/ALR", Homepage: "https://gitea.plemya-x.ru/xpamych/ALR",
Maintainer: "Евгений Храмов <xpamych@yandex.ru>", Maintainer: "Евгений Храмов <xpamych@yandex.ru>",
Architectures: []string{"arm64", "amd64"}, Architectures: []string{"arm64", "amd64"},
Licenses: []string{"GPL-3.0-or-later"}, Licenses: []string{"GPL-3.0-or-later"},
Provides: []string{"test"}, Provides: []string{"test"},
Conflicts: []string{"test"}, Conflicts: []string{"test"},
Replaces: []string{"test-legacy"}, Replaces: alrsh.OverridableFromMap(map[string][]string{
Depends: []string{"sudo"}, "": {"test-old"},
BuildDepends: []string{"go"}, "test_os": {"test-legacy"},
}),
Depends: []string{"sudo"},
BuildDepends: alrsh.OverridableFromMap(map[string][]string{
"": {"golang"},
"arch": {"go"},
}),
} }
expected.Description.SetResolved("Test package")
expected.Replaces.SetResolved([]string{"test-legacy"})
expected.BuildDepends.SetResolved([]string{"go"})
if !reflect.DeepEqual(bv, expected) { if !reflect.DeepEqual(bv, expected) {
t.Errorf("Expected %v, got %v", expected, bv) t.Errorf("Expected %v, got %v", expected, bv)
} }

View File

@@ -0,0 +1,53 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package helpers
import (
"io/fs"
"os"
"path/filepath"
)
// dirLfs implements fs.FS like os.DirFS but uses LStat instead of Stat.
// This means symbolic links are treated as links themselves rather than
// being followed to their targets.
type dirLfs struct {
fs.FS
dir string
}
func NewDirLFS(dir string) *dirLfs {
return &dirLfs{
FS: os.DirFS(dir),
dir: dir,
}
}
func (d *dirLfs) Stat(name string) (fs.FileInfo, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrInvalid}
}
fullPath := filepath.Join(d.dir, filepath.FromSlash(name))
info, err := os.Lstat(fullPath)
if err != nil {
return nil, &fs.PathError{Op: "stat", Path: name, Err: err}
}
return info, nil
}

View File

@@ -18,13 +18,13 @@ package helpers
import ( import (
"fmt" "fmt"
"log/slog"
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"github.com/bmatcuk/doublestar/v4" "github.com/bmatcuk/doublestar/v4"
"mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
) )
func matchNamePattern(name, pattern string) bool { func matchNamePattern(name, pattern string) bool {
@@ -46,10 +46,15 @@ func validateDir(dirPath, commandName string) error {
return nil return nil
} }
func outputFiles(hc interp.HandlerContext, files []string) { func outputFiles(hc interp.HandlerContext, files []string) error {
for _, file := range files { for _, file := range files {
fmt.Fprintln(hc.Stdout, file) v, err := syntax.Quote(file, syntax.LangAuto)
if err != nil {
return err
}
fmt.Fprintln(hc.Stdout, v)
} }
return nil
} }
func makeRelativePath(basePath, fullPath string) (string, error) { func makeRelativePath(basePath, fullPath string) (string, error) {
@@ -92,8 +97,7 @@ func filesFindLangCmd(hc interp.HandlerContext, cmd string, args []string) error
return fmt.Errorf("files-find-lang: %w", err) return fmt.Errorf("files-find-lang: %w", err)
} }
outputFiles(hc, langFiles) return outputFiles(hc, langFiles)
return nil
} }
func filesFindDocCmd(hc interp.HandlerContext, cmd string, args []string) error { func filesFindDocCmd(hc interp.HandlerContext, cmd string, args []string) error {
@@ -142,13 +146,12 @@ func filesFindDocCmd(hc interp.HandlerContext, cmd string, args []string) error
} }
} }
outputFiles(hc, docFiles) return outputFiles(hc, docFiles)
return nil
} }
func filesFindCmd(hc interp.HandlerContext, cmd string, args []string) error { func filesFindCmd(hc interp.HandlerContext, cmd string, args []string) error {
if len(args) == 0 { if len(args) == 0 {
return fmt.Errorf("find-files: at least one glob pattern is required") return fmt.Errorf("files-find: at least one glob pattern is required")
} }
var foundFiles []string var foundFiles []string
@@ -157,11 +160,10 @@ func filesFindCmd(hc interp.HandlerContext, cmd string, args []string) error {
searchPath := path.Join(hc.Dir, globPattern) searchPath := path.Join(hc.Dir, globPattern)
basepath, pattern := doublestar.SplitPattern(searchPath) basepath, pattern := doublestar.SplitPattern(searchPath)
fsys := os.DirFS(basepath) fsys := NewDirLFS(basepath)
matches, err := doublestar.Glob(fsys, pattern, doublestar.WithNoFollow()) matches, err := doublestar.Glob(fsys, pattern, doublestar.WithNoFollow(), doublestar.WithFailOnPatternNotExist())
if err != nil { if err != nil {
slog.Warn("find-files: invalid glob pattern", "pattern", globPattern, "error", err) return fmt.Errorf("files-find: glob pattern error: %w", err)
continue
} }
for _, match := range matches { for _, match := range matches {
@@ -173,6 +175,5 @@ func filesFindCmd(hc interp.HandlerContext, cmd string, args []string) error {
} }
} }
outputFiles(hc, foundFiles) return outputFiles(hc, foundFiles)
return nil
} }

View File

@@ -24,6 +24,8 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/bmatcuk/doublestar/v4"
"github.com/google/shlex"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"mvdan.cc/sh/v3/interp" "mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax" "mvdan.cc/sh/v3/syntax"
@@ -43,6 +45,7 @@ type testCase struct {
expectedOutput []string expectedOutput []string
symlinksToCreate []symlink symlinksToCreate []symlink
args string args string
expectedError error
} }
func TestFindFilesDoc(t *testing.T) { func TestFindFilesDoc(t *testing.T) {
@@ -131,7 +134,8 @@ files-find-doc ` + tc.args
err = runner.Run(context.Background(), script) err = runner.Run(context.Background(), script)
assert.NoError(t, err) assert.NoError(t, err)
contents := strings.Fields(strings.TrimSpace(buf.String())) contents, err := shlex.Split(buf.String())
assert.NoError(t, err)
assert.ElementsMatch(t, tc.expectedOutput, contents) assert.ElementsMatch(t, tc.expectedOutput, contents)
}) })
} }
@@ -215,7 +219,8 @@ files-find-lang ` + tc.args
err = runner.Run(context.Background(), script) err = runner.Run(context.Background(), script)
assert.NoError(t, err) assert.NoError(t, err)
contents := strings.Fields(strings.TrimSpace(buf.String())) contents, err := shlex.Split(buf.String())
assert.NoError(t, err)
assert.ElementsMatch(t, tc.expectedOutput, contents) assert.ElementsMatch(t, tc.expectedOutput, contents)
}) })
} }
@@ -230,18 +235,25 @@ func TestFindFiles(t *testing.T) {
"usr/share/locale/tr/LC_MESSAGES", "usr/share/locale/tr/LC_MESSAGES",
"opt/app", "opt/app",
"opt/app/internal", "opt/app/internal",
"opt/app/with space",
"usr/bin",
}, },
filesToCreate: []string{ filesToCreate: []string{
"usr/share/locale/ru/LC_MESSAGES/yandex-disk.mo", "usr/share/locale/ru/LC_MESSAGES/yandex-disk.mo",
"usr/share/locale/ru/LC_MESSAGES/yandex-disk-indicator.mo", "usr/share/locale/ru/LC_MESSAGES/yandex-disk-indicator.mo",
"usr/share/locale/tr/LC_MESSAGES/yandex-disk.mo", "usr/share/locale/tr/LC_MESSAGES/yandex-disk.mo",
"opt/app/internal/test", "opt/app/internal/test",
"opt/app/with space/file",
}, },
symlinksToCreate: []symlink{ symlinksToCreate: []symlink{
{ {
linkPath: "/opt/app/etc", linkPath: "/opt/app/etc",
targetPath: "/etc", targetPath: "/etc",
}, },
{
linkPath: "/usr/bin/file",
targetPath: "/not-existing",
},
}, },
expectedOutput: []string{ expectedOutput: []string{
"./usr/share/locale/ru/LC_MESSAGES/yandex-disk.mo", "./usr/share/locale/ru/LC_MESSAGES/yandex-disk.mo",
@@ -250,8 +262,17 @@ func TestFindFiles(t *testing.T) {
"./opt/app/etc", "./opt/app/etc",
"./opt/app/internal", "./opt/app/internal",
"./opt/app/internal/test", "./opt/app/internal/test",
"./opt/app/with space",
"./opt/app/with space/file",
"./usr/bin/file",
}, },
args: "\"/usr/share/locale/*/LC_MESSAGES/*.mo\" \"/opt/app/**/*\"", args: "\"/usr/share/locale/*/LC_MESSAGES/*.mo\" \"/opt/app/**/*\" \"/usr/bin/file\"",
expectedError: nil,
},
{
name: "Not existing paths should throw error",
args: "\"/opt/test/not-existing\"",
expectedError: doublestar.ErrPatternNotExist,
}, },
} }
@@ -304,9 +325,14 @@ files-find ` + tc.args
assert.NoError(t, err) assert.NoError(t, err)
err = runner.Run(context.Background(), script) err = runner.Run(context.Background(), script)
assert.NoError(t, err) if tc.expectedError != nil {
assert.ErrorAs(t, err, &tc.expectedError)
} else {
assert.NoError(t, err)
}
contents := strings.Fields(strings.TrimSpace(buf.String())) contents, err := shlex.Split(buf.String())
assert.NoError(t, err)
assert.ElementsMatch(t, tc.expectedOutput, contents) assert.ElementsMatch(t, tc.expectedOutput, contents)
}) })
} }

106
internal/stats/tracker.go Normal file
View File

@@ -0,0 +1,106 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package stats
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
)
type InstallationData struct {
PackageName string `json:"packageName"`
Version string `json:"version,omitempty"`
InstallType string `json:"installType"` // "install" or "upgrade"
UserAgent string `json:"userAgent"`
Fingerprint string `json:"fingerprint,omitempty"`
}
var (
apiEndpoints = []string{
"https://alr.plemya-x.ru/api/packages/track-install",
"http://localhost:3001/api/packages/track-install",
}
userAgent = "ALR-CLI/1.0"
)
func generateFingerprint(packageName string) string {
hostname, _ := os.Hostname()
data := fmt.Sprintf("%s_%s_%s", hostname, packageName, time.Now().Format("2006-01-02"))
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:])
}
// TrackInstallation отправляет статистику установки пакета
func TrackInstallation(ctx context.Context, packageName string, installType string) {
// Запускаем в отдельной горутине, чтобы не блокировать основной процесс
go func() {
data := InstallationData{
PackageName: packageName,
InstallType: installType,
UserAgent: userAgent,
Fingerprint: generateFingerprint(packageName),
}
jsonData, err := json.Marshal(data)
if err != nil {
return // Тихо игнорируем ошибки - статистика не критична
}
// Пробуем отправить запрос к разным endpoint-ам
for _, endpoint := range apiEndpoints {
if sendRequest(endpoint, jsonData) {
return // Если хотя бы один запрос прошёл успешно, выходим
}
}
}()
}
func sendRequest(endpoint string, data []byte) bool {
client := &http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(data))
if err != nil {
return false
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode >= 200 && resp.StatusCode < 300
}
// ShouldTrackPackage проверяет, нужно ли отслеживать установку этого пакета
func ShouldTrackPackage(packageName string) bool {
// Отслеживаем только alr-bin
return strings.Contains(packageName, "alr-bin")
}

View File

@@ -9,55 +9,99 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: build.go:42 #: build.go:41
msgid "Build a local package" msgid "Build a local package"
msgstr "" msgstr ""
#: build.go:48 #: build.go:47
msgid "Path to the build script" msgid "Path to the build script"
msgstr "" msgstr ""
#: build.go:53 #: build.go:52
msgid "Specify subpackage in script (for multi package script only)" msgid "Specify subpackage in script (for multi package script only)"
msgstr "" msgstr ""
#: build.go:58 #: build.go:57
msgid "Name of the package to build and its repo (example: default/go-bin)" msgid "Name of the package to build and its repo (example: default/go-bin)"
msgstr "" msgstr ""
#: build.go:63 #: build.go:62
msgid "" msgid ""
"Build package from scratch even if there's an already built package available" "Build package from scratch even if there's an already built package available"
msgstr "" msgstr ""
#: build.go:73 #: build.go:72
msgid "Error getting working directory" msgid "Error getting working directory"
msgstr "" msgstr ""
#: build.go:118 #: build.go:117
msgid "Cannot get absolute script path" msgid "Cannot get absolute script path"
msgstr "" msgstr ""
#: build.go:152 #: build.go:143
msgid "Package not found" msgid "Package not found"
msgstr "" msgstr ""
#: build.go:165 #: build.go:156
msgid "Nothing to build" msgid "Nothing to build"
msgstr "" msgstr ""
#: build.go:222 #: build.go:213
msgid "Error building package" msgid "Error building package"
msgstr "" msgstr ""
#: build.go:229 #: build.go:220
msgid "Error moving the package" msgid "Error moving the package"
msgstr "" msgstr ""
#: build.go:233 #: build.go:224
msgid "Done" msgid "Done"
msgstr "" msgstr ""
#: config.go:36
msgid "Manage config"
msgstr ""
#: config.go:48
msgid "Show config"
msgstr ""
#: config.go:84
msgid "Set config value"
msgstr ""
#: config.go:85
msgid "<key> <value>"
msgstr ""
#: config.go:118 config.go:126
msgid "invalid boolean value for %s: %s"
msgstr ""
#: config.go:141
msgid "use 'repo add/remove' commands to manage repositories"
msgstr ""
#: config.go:143 config.go:221
msgid "unknown config key: %s"
msgstr ""
#: config.go:147
msgid "failed to save config"
msgstr ""
#: config.go:150
msgid "Successfully set %s = %s"
msgstr ""
#: config.go:159
msgid "Get config value"
msgstr ""
#: config.go:160
msgid "<key>"
msgstr ""
#: fix.go:39 #: fix.go:39
msgid "Attempt to fix problems with ALR" msgid "Attempt to fix problems with ALR"
msgstr "" msgstr ""
@@ -138,11 +182,11 @@ msgstr ""
msgid "Can't detect system language" msgid "Can't detect system language"
msgstr "" msgstr ""
#: info.go:135 #: info.go:134
msgid "Error resolving overrides" msgid "Error resolving overrides"
msgstr "" msgstr ""
#: info.go:144 info.go:149 #: info.go:143
msgid "Error encoding script variables" msgid "Error encoding script variables"
msgstr "" msgstr ""
@@ -174,19 +218,23 @@ msgstr ""
msgid "Error removing packages" msgid "Error removing packages"
msgstr "" msgstr ""
#: internal/build/build.go:376 #: internal/build/build.go:351
msgid "Building package" msgid "Building package"
msgstr "" msgstr ""
#: internal/build/build.go:405 #: internal/build/build.go:380
msgid "The checksums array must be the same length as sources" msgid "The checksums array must be the same length as sources"
msgstr "" msgstr ""
#: internal/build/build.go:447 #: internal/build/build.go:422
msgid "Downloading sources" msgid "Downloading sources"
msgstr "" msgstr ""
#: internal/build/build.go:539 #: internal/build/build.go:468
msgid "Would you like to remove the build dependencies?"
msgstr ""
#: internal/build/build.go:546
msgid "Installing dependencies" msgid "Installing dependencies"
msgstr "" msgstr ""
@@ -355,47 +403,31 @@ msgid ""
"Database version does not exist. Run alr fix if something isn't working." "Database version does not exist. Run alr fix if something isn't working."
msgstr "" msgstr ""
#: internal/dl/dl.go:170
msgid "Source can be updated, updating if required"
msgstr ""
#: internal/dl/dl.go:201
msgid "Source found in cache and linked to destination"
msgstr ""
#: internal/dl/dl.go:208
msgid "Source updated and linked to destination"
msgstr ""
#: internal/dl/dl.go:222
msgid "Downloading source"
msgstr ""
#: internal/dl/progress_tui.go:100
msgid "%s: done!\n"
msgstr ""
#: internal/dl/progress_tui.go:104
msgid "%s %s downloading at %s/s\n"
msgstr ""
#: internal/logger/log.go:41 #: internal/logger/log.go:41
msgid "ERROR" msgid "ERROR"
msgstr "" msgstr ""
#: internal/repos/pull.go:77 #: internal/repos/pull.go:97
msgid "Trying mirror"
msgstr ""
#: internal/repos/pull.go:103
msgid "Failed to pull from URL"
msgstr ""
#: internal/repos/pull.go:167
msgid "Pulling repository" msgid "Pulling repository"
msgstr "" msgstr ""
#: internal/repos/pull.go:113 #: internal/repos/pull.go:204
msgid "Repository up to date" msgid "Repository up to date"
msgstr "" msgstr ""
#: internal/repos/pull.go:204 #: internal/repos/pull.go:239
msgid "Git repository does not appear to be a valid ALR repo" msgid "Git repository does not appear to be a valid ALR repo"
msgstr "" msgstr ""
#: internal/repos/pull.go:220 #: internal/repos/pull.go:255
msgid "" msgid ""
"ALR repo's minimum ALR version is greater than the current version. Try " "ALR repo's minimum ALR version is greater than the current version. Try "
"updating ALR if something doesn't work." "updating ALR if something doesn't work."
@@ -413,30 +445,34 @@ msgstr ""
msgid "You need to be root to perform this action" msgid "You need to be root to perform this action"
msgstr "" msgstr ""
#: list.go:43 #: list.go:45
msgid "List ALR repo packages" msgid "List ALR repo packages"
msgstr "" msgstr ""
#: list.go:57 #: list.go:59
msgid "Format output using a Go template" msgid "Format output using a Go template"
msgstr "" msgstr ""
#: list.go:89 #: list.go:91
msgid "Error getting packages for upgrade" msgid "Error getting packages for upgrade"
msgstr "" msgstr ""
#: list.go:92 #: list.go:94
msgid "No packages for upgrade" msgid "No packages for upgrade"
msgstr "" msgstr ""
#: list.go:102 list.go:184 #: list.go:104 list.go:201
msgid "Error parsing format template" msgid "Error parsing format template"
msgstr "" msgstr ""
#: list.go:108 list.go:188 #: list.go:110 list.go:205
msgid "Error executing template" msgid "Error executing template"
msgstr "" msgstr ""
#: list.go:164
msgid "Failed to parse release"
msgstr ""
#: main.go:45 #: main.go:45
msgid "Print the current ALR version and exit" msgid "Print the current ALR version and exit"
msgstr "" msgstr ""
@@ -449,75 +485,140 @@ msgstr ""
msgid "Enable interactive questions and prompts" msgid "Enable interactive questions and prompts"
msgstr "" msgstr ""
#: main.go:146 #: main.go:148
msgid "Show help" msgid "Show help"
msgstr "" msgstr ""
#: main.go:150 #: main.go:152
msgid "Error while running app" msgid "Error while running app"
msgstr "" msgstr ""
#: pkg/dl/dl.go:170
msgid "Source can be updated, updating if required"
msgstr ""
#: pkg/dl/dl.go:196
msgid "Source found in cache and linked to destination"
msgstr ""
#: pkg/dl/dl.go:203
msgid "Source updated and linked to destination"
msgstr ""
#: pkg/dl/dl.go:217
msgid "Downloading source"
msgstr ""
#: pkg/dl/progress_tui.go:100
msgid "%s: done!\n"
msgstr ""
#: pkg/dl/progress_tui.go:104
msgid "%s %s downloading at %s/s\n"
msgstr ""
#: refresh.go:30 #: refresh.go:30
msgid "Pull all repositories that have changed" msgid "Pull all repositories that have changed"
msgstr "" msgstr ""
#: repo.go:39 #: repo.go:42
msgid "Manage repos" msgid "Manage repos"
msgstr "" msgstr ""
#: repo.go:51 repo.go:269 #: repo.go:56 repo.go:625
msgid "Remove an existing repository" msgid "Remove an existing repository"
msgstr "" msgstr ""
#: repo.go:53 #: repo.go:58 repo.go:521
msgid "<name>" msgid "<name>"
msgstr "" msgstr ""
#: repo.go:83 #: repo.go:103 repo.go:465 repo.go:568
msgid "Repo \"%s\" does not exist" msgid "Repo \"%s\" does not exist"
msgstr "" msgstr ""
#: repo.go:90 #: repo.go:110
msgid "Error removing repo directory" msgid "Error removing repo directory"
msgstr "" msgstr ""
#: repo.go:94 repo.go:161 repo.go:219 #: repo.go:114 repo.go:195 repo.go:253 repo.go:316 repo.go:389 repo.go:504
#: repo.go:576
msgid "Error saving config" msgid "Error saving config"
msgstr "" msgstr ""
#: repo.go:113 #: repo.go:133
msgid "Error removing packages from database" msgid "Error removing packages from database"
msgstr "" msgstr ""
#: repo.go:124 repo.go:239 #: repo.go:144 repo.go:595
msgid "Add a new repository" msgid "Add a new repository"
msgstr "" msgstr ""
#: repo.go:125 #: repo.go:145 repo.go:270 repo.go:345 repo.go:402
msgid "<name> <url>" msgid "<name> <url>"
msgstr "" msgstr ""
#: repo.go:150 #: repo.go:170
msgid "Repo \"%s\" already exists" msgid "Repo \"%s\" already exists"
msgstr "" msgstr ""
#: repo.go:187 #: repo.go:206
msgid "Set the reference of the repository" msgid "Set the reference of the repository"
msgstr "" msgstr ""
#: repo.go:188 #: repo.go:207
msgid "<name> <ref>" msgid "<name> <ref>"
msgstr "" msgstr ""
#: repo.go:246 #: repo.go:269
msgid "Set the main url of the repository"
msgstr ""
#: repo.go:332
msgid "Manage mirrors of repos"
msgstr ""
#: repo.go:344
msgid "Add a mirror URL to repository"
msgstr ""
#: repo.go:401
msgid "Remove mirror from the repository"
msgstr ""
#: repo.go:420
msgid "Ignore if mirror does not exist"
msgstr ""
#: repo.go:425
msgid "Match partial URL (e.g., github.com instead of full URL)"
msgstr ""
#: repo.go:490
msgid "No mirrors containing \"%s\" found in repo \"%s\""
msgstr ""
#: repo.go:492
msgid "URL \"%s\" does not exist in repo \"%s\""
msgstr ""
#: repo.go:508 repo.go:580
msgid "Removed %d mirrors from repo \"%s\"\n"
msgstr ""
#: repo.go:520
msgid "Remove all mirrors from the repository"
msgstr ""
#: repo.go:602
msgid "Name of the new repo" msgid "Name of the new repo"
msgstr "" msgstr ""
#: repo.go:252 #: repo.go:608
msgid "URL of the new repo" msgid "URL of the new repo"
msgstr "" msgstr ""
#: repo.go:276 #: repo.go:632
msgid "Name of the repo to be deleted" msgid "Name of the repo to be deleted"
msgstr "" msgstr ""

View File

@@ -5,66 +5,110 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: unnamed project\n" "Project-Id-Version: unnamed project\n"
"PO-Revision-Date: 2025-06-15 16:05+0300\n" "PO-Revision-Date: 2025-07-09 20:38+0300\n"
"Last-Translator: Maxim Slipenko <maks1ms@alt-gnome.ru>\n" "Last-Translator: Maxim Slipenko <maks1ms@alt-gnome.ru>\n"
"Language-Team: Russian\n" "Language-Team: Russian\n"
"Language: ru\n" "Language: ru\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Gtranslator 48.0\n" "X-Generator: Gtranslator 48.0\n"
#: build.go:42 #: build.go:41
msgid "Build a local package" msgid "Build a local package"
msgstr "Сборка локального пакета" msgstr "Сборка локального пакета"
#: build.go:48 #: build.go:47
msgid "Path to the build script" msgid "Path to the build script"
msgstr "Путь к скрипту сборки" msgstr "Путь к скрипту сборки"
#: build.go:53 #: build.go:52
msgid "Specify subpackage in script (for multi package script only)" msgid "Specify subpackage in script (for multi package script only)"
msgstr "Укажите подпакет в скрипте (только для многопакетного скрипта)" msgstr "Укажите подпакет в скрипте (только для многопакетного скрипта)"
#: build.go:58 #: build.go:57
msgid "Name of the package to build and its repo (example: default/go-bin)" msgid "Name of the package to build and its repo (example: default/go-bin)"
msgstr "Имя пакета для сборки и его репозиторий (пример: default/go-bin)" msgstr "Имя пакета для сборки и его репозиторий (пример: default/go-bin)"
#: build.go:63 #: build.go:62
msgid "" msgid ""
"Build package from scratch even if there's an already built package available" "Build package from scratch even if there's an already built package available"
msgstr "Создайте пакет с нуля, даже если уже имеется готовый пакет" msgstr "Создайте пакет с нуля, даже если уже имеется готовый пакет"
#: build.go:73 #: build.go:72
msgid "Error getting working directory" msgid "Error getting working directory"
msgstr "Ошибка при получении рабочего каталога" msgstr "Ошибка при получении рабочего каталога"
#: build.go:118 #: build.go:117
msgid "Cannot get absolute script path" msgid "Cannot get absolute script path"
msgstr "Невозможно получить абсолютный путь к скрипту" msgstr "Невозможно получить абсолютный путь к скрипту"
#: build.go:152 #: build.go:143
msgid "Package not found" msgid "Package not found"
msgstr "Пакет не найден" msgstr "Пакет не найден"
#: build.go:165 #: build.go:156
msgid "Nothing to build" msgid "Nothing to build"
msgstr "Нечего собирать" msgstr "Нечего собирать"
#: build.go:222 #: build.go:213
msgid "Error building package" msgid "Error building package"
msgstr "Ошибка при сборке пакета" msgstr "Ошибка при сборке пакета"
#: build.go:229 #: build.go:220
msgid "Error moving the package" msgid "Error moving the package"
msgstr "Ошибка при перемещении пакета" msgstr "Ошибка при перемещении пакета"
#: build.go:233 #: build.go:224
msgid "Done" msgid "Done"
msgstr "Сделано" msgstr "Сделано"
#: config.go:36
msgid "Manage config"
msgstr "Управление конфигурацией"
#: config.go:48
msgid "Show config"
msgstr "Показать конфигурацию"
#: config.go:84
msgid "Set config value"
msgstr "Установить значение в конфигурации"
#: config.go:85
msgid "<key> <value>"
msgstr "<ключ> <значение>"
#: config.go:118 config.go:126
msgid "invalid boolean value for %s: %s"
msgstr "неверное булево значение для %s: %s"
#: config.go:141
msgid "use 'repo add/remove' commands to manage repositories"
msgstr "используйте команды 'repo add/remove' для управления репозиториями"
#: config.go:143 config.go:221
msgid "unknown config key: %s"
msgstr "неизвестный ключ конфигурации: %s"
#: config.go:147
msgid "failed to save config"
msgstr "не удалось сохранить конфигурацию"
#: config.go:150
msgid "Successfully set %s = %s"
msgstr "Успешно установлено %s = %s"
#: config.go:159
msgid "Get config value"
msgstr "Получить значение из конфигурации"
#: config.go:160
msgid "<key>"
msgstr "<ключ>"
#: fix.go:39 #: fix.go:39
msgid "Attempt to fix problems with ALR" msgid "Attempt to fix problems with ALR"
msgstr "Попытка устранить проблемы с ALR" msgstr "Попытка устранить проблемы с ALR"
@@ -145,11 +189,11 @@ msgstr "Ошибка при поиске пакетов"
msgid "Can't detect system language" msgid "Can't detect system language"
msgstr "Ошибка при определении языка системы" msgstr "Ошибка при определении языка системы"
#: info.go:135 #: info.go:134
msgid "Error resolving overrides" msgid "Error resolving overrides"
msgstr "Ошибка устранения переорпеделений" msgstr "Ошибка устранения переорпеделений"
#: info.go:144 info.go:149 #: info.go:143
msgid "Error encoding script variables" msgid "Error encoding script variables"
msgstr "Ошибка кодирования переменных скрита" msgstr "Ошибка кодирования переменных скрита"
@@ -181,19 +225,23 @@ msgstr "Для команды remove ожидался хотя бы 1 аргум
msgid "Error removing packages" msgid "Error removing packages"
msgstr "Ошибка при удалении пакетов" msgstr "Ошибка при удалении пакетов"
#: internal/build/build.go:376 #: internal/build/build.go:351
msgid "Building package" msgid "Building package"
msgstr "Сборка пакета" msgstr "Сборка пакета"
#: internal/build/build.go:405 #: internal/build/build.go:380
msgid "The checksums array must be the same length as sources" msgid "The checksums array must be the same length as sources"
msgstr "Массив контрольных сумм должен быть той же длины, что и источники" msgstr "Массив контрольных сумм должен быть той же длины, что и источники"
#: internal/build/build.go:447 #: internal/build/build.go:422
msgid "Downloading sources" msgid "Downloading sources"
msgstr "Скачивание источников" msgstr "Скачивание источников"
#: internal/build/build.go:539 #: internal/build/build.go:468
msgid "Would you like to remove the build dependencies?"
msgstr "Хотели бы вы удалить зависимости сборки?"
#: internal/build/build.go:546
msgid "Installing dependencies" msgid "Installing dependencies"
msgstr "Установка зависимостей" msgstr "Установка зависимостей"
@@ -356,8 +404,8 @@ msgid ""
"This command is deprecated and would be removed in the future, use \"%s\" " "This command is deprecated and would be removed in the future, use \"%s\" "
"instead!" "instead!"
msgstr "" msgstr ""
"Эта команда устарела и будет удалена в будущем, используйте вместо нее \"%s" "Эта команда устарела и будет удалена в будущем, используйте вместо нее "
"\"!" "\"%s\"!"
#: internal/db/db.go:76 #: internal/db/db.go:76
msgid "Database version mismatch; resetting" msgid "Database version mismatch; resetting"
@@ -369,47 +417,31 @@ msgid ""
msgstr "" msgstr ""
"Версия базы данных не существует. Запустите alr fix, если что-то не работает." "Версия базы данных не существует. Запустите alr fix, если что-то не работает."
#: internal/dl/dl.go:170
msgid "Source can be updated, updating if required"
msgstr "Исходный код можно обновлять, обновляя при необходимости"
#: internal/dl/dl.go:201
msgid "Source found in cache and linked to destination"
msgstr "Источник найден в кэше и связан с пунктом назначения"
#: internal/dl/dl.go:208
msgid "Source updated and linked to destination"
msgstr "Источник обновлён и связан с пунктом назначения"
#: internal/dl/dl.go:222
msgid "Downloading source"
msgstr "Скачивание источника"
#: internal/dl/progress_tui.go:100
msgid "%s: done!\n"
msgstr "%s: выполнено!\n"
#: internal/dl/progress_tui.go:104
msgid "%s %s downloading at %s/s\n"
msgstr "%s %s загружается — %s/с\n"
#: internal/logger/log.go:41 #: internal/logger/log.go:41
msgid "ERROR" msgid "ERROR"
msgstr "ОШИБКА" msgstr "ОШИБКА"
#: internal/repos/pull.go:77 #: internal/repos/pull.go:97
msgid "Trying mirror"
msgstr "Пробую зеркало"
#: internal/repos/pull.go:103
msgid "Failed to pull from URL"
msgstr "Не удалось извлечь из URL"
#: internal/repos/pull.go:167
msgid "Pulling repository" msgid "Pulling repository"
msgstr "Скачивание репозитория" msgstr "Скачивание репозитория"
#: internal/repos/pull.go:113 #: internal/repos/pull.go:204
msgid "Repository up to date" msgid "Repository up to date"
msgstr "Репозиторий уже обновлён" msgstr "Репозиторий уже обновлён"
#: internal/repos/pull.go:204 #: internal/repos/pull.go:239
msgid "Git repository does not appear to be a valid ALR repo" msgid "Git repository does not appear to be a valid ALR repo"
msgstr "Репозиторий Git не поддерживается репозиторием ALR" msgstr "Репозиторий Git не поддерживается репозиторием ALR"
#: internal/repos/pull.go:220 #: internal/repos/pull.go:255
msgid "" msgid ""
"ALR repo's minimum ALR version is greater than the current version. Try " "ALR repo's minimum ALR version is greater than the current version. Try "
"updating ALR if something doesn't work." "updating ALR if something doesn't work."
@@ -429,30 +461,34 @@ msgstr "Вы должны быть членом %s чтобы выполнить
msgid "You need to be root to perform this action" msgid "You need to be root to perform this action"
msgstr "Вы должны быть root чтобы выполнить это" msgstr "Вы должны быть root чтобы выполнить это"
#: list.go:43 #: list.go:45
msgid "List ALR repo packages" msgid "List ALR repo packages"
msgstr "Список пакетов репозитория ALR" msgstr "Список пакетов репозитория ALR"
#: list.go:57 #: list.go:59
msgid "Format output using a Go template" msgid "Format output using a Go template"
msgstr "Формат выходных данных с использованием шаблона Go" msgstr "Формат выходных данных с использованием шаблона Go"
#: list.go:89 #: list.go:91
msgid "Error getting packages for upgrade" msgid "Error getting packages for upgrade"
msgstr "Ошибка при получении пакетов для обновления" msgstr "Ошибка при получении пакетов для обновления"
#: list.go:92 #: list.go:94
msgid "No packages for upgrade" msgid "No packages for upgrade"
msgstr "Нет пакетов к обновлению" msgstr "Нет пакетов к обновлению"
#: list.go:102 list.go:184 #: list.go:104 list.go:201
msgid "Error parsing format template" msgid "Error parsing format template"
msgstr "Ошибка при разборе шаблона" msgstr "Ошибка при разборе шаблона"
#: list.go:108 list.go:188 #: list.go:110 list.go:205
msgid "Error executing template" msgid "Error executing template"
msgstr "Ошибка при выполнении шаблона" msgstr "Ошибка при выполнении шаблона"
#: list.go:164
msgid "Failed to parse release"
msgstr "Не удалось разобрать релиз"
#: main.go:45 #: main.go:45
msgid "Print the current ALR version and exit" msgid "Print the current ALR version and exit"
msgstr "Показать текущую версию ALR и выйти" msgstr "Показать текущую версию ALR и выйти"
@@ -465,75 +501,140 @@ msgstr "Аргументы, которые будут переданы мене
msgid "Enable interactive questions and prompts" msgid "Enable interactive questions and prompts"
msgstr "Включение интерактивных вопросов и запросов" msgstr "Включение интерактивных вопросов и запросов"
#: main.go:146 #: main.go:148
msgid "Show help" msgid "Show help"
msgstr "Показать справку" msgstr "Показать справку"
#: main.go:150 #: main.go:152
msgid "Error while running app" msgid "Error while running app"
msgstr "Ошибка при запуске приложения" msgstr "Ошибка при запуске приложения"
#: pkg/dl/dl.go:170
msgid "Source can be updated, updating if required"
msgstr "Исходный код можно обновлять, обновляя при необходимости"
#: pkg/dl/dl.go:196
msgid "Source found in cache and linked to destination"
msgstr "Источник найден в кэше и связан с пунктом назначения"
#: pkg/dl/dl.go:203
msgid "Source updated and linked to destination"
msgstr "Источник обновлён и связан с пунктом назначения"
#: pkg/dl/dl.go:217
msgid "Downloading source"
msgstr "Скачивание источника"
#: pkg/dl/progress_tui.go:100
msgid "%s: done!\n"
msgstr "%s: выполнено!\n"
#: pkg/dl/progress_tui.go:104
msgid "%s %s downloading at %s/s\n"
msgstr "%s %s загружается — %s/с\n"
#: refresh.go:30 #: refresh.go:30
msgid "Pull all repositories that have changed" msgid "Pull all repositories that have changed"
msgstr "Скачать все изменённые репозитории" msgstr "Скачать все изменённые репозитории"
#: repo.go:39 #: repo.go:42
msgid "Manage repos" msgid "Manage repos"
msgstr "Управление репозиториями" msgstr "Управление репозиториями"
#: repo.go:51 repo.go:269 #: repo.go:56 repo.go:625
msgid "Remove an existing repository" msgid "Remove an existing repository"
msgstr "Удалить существующий репозиторий" msgstr "Удалить существующий репозиторий"
#: repo.go:53 #: repo.go:58 repo.go:521
msgid "<name>" msgid "<name>"
msgstr "<имя>" msgstr "<имя>"
#: repo.go:83 #: repo.go:103 repo.go:465 repo.go:568
msgid "Repo \"%s\" does not exist" msgid "Repo \"%s\" does not exist"
msgstr "Репозитория \"%s\" не существует" msgstr "Репозитория \"%s\" не существует"
#: repo.go:90 #: repo.go:110
msgid "Error removing repo directory" msgid "Error removing repo directory"
msgstr "Ошибка при удалении каталога репозитория" msgstr "Ошибка при удалении каталога репозитория"
#: repo.go:94 repo.go:161 repo.go:219 #: repo.go:114 repo.go:195 repo.go:253 repo.go:316 repo.go:389 repo.go:504
#: repo.go:576
msgid "Error saving config" msgid "Error saving config"
msgstr "Ошибка при сохранении конфигурации" msgstr "Ошибка при сохранении конфигурации"
#: repo.go:113 #: repo.go:133
msgid "Error removing packages from database" msgid "Error removing packages from database"
msgstr "Ошибка при удалении пакетов из базы данных" msgstr "Ошибка при удалении пакетов из базы данных"
#: repo.go:124 repo.go:239 #: repo.go:144 repo.go:595
msgid "Add a new repository" msgid "Add a new repository"
msgstr "Добавить новый репозиторий" msgstr "Добавить новый репозиторий"
#: repo.go:125 #: repo.go:145 repo.go:270 repo.go:345 repo.go:402
msgid "<name> <url>" msgid "<name> <url>"
msgstr "<имя> <url>" msgstr "<имя> <url>"
#: repo.go:150 #: repo.go:170
msgid "Repo \"%s\" already exists" msgid "Repo \"%s\" already exists"
msgstr "Репозиторий \"%s\" уже существует" msgstr "Репозиторий \"%s\" уже существует"
#: repo.go:187 #: repo.go:206
msgid "Set the reference of the repository" msgid "Set the reference of the repository"
msgstr "Установить ссылку на версию репозитория" msgstr "Установить ссылку на версию репозитория"
#: repo.go:188 #: repo.go:207
msgid "<name> <ref>" msgid "<name> <ref>"
msgstr "<имя> <ссылкааерсию>" msgstr "<имя> <ссылкааерсию>"
#: repo.go:246 #: repo.go:269
msgid "Set the main url of the repository"
msgstr "Установить главный URL репозитория"
#: repo.go:332
msgid "Manage mirrors of repos"
msgstr "Управление зеркалами репозитория"
#: repo.go:344
msgid "Add a mirror URL to repository"
msgstr "Добавить зеркало репозитория"
#: repo.go:401
msgid "Remove mirror from the repository"
msgstr "Удалить зеркало из репозитория"
#: repo.go:420
msgid "Ignore if mirror does not exist"
msgstr "Игнорировать, если зеркала не существует"
#: repo.go:425
msgid "Match partial URL (e.g., github.com instead of full URL)"
msgstr "Соответствует частичному URL (например, github.com вместо полного URL)"
#: repo.go:490
msgid "No mirrors containing \"%s\" found in repo \"%s\""
msgstr "В репозитории \"%s\" не найдено зеркал, содержащих \"%s\""
#: repo.go:492
msgid "URL \"%s\" does not exist in repo \"%s\""
msgstr "URL \"%s\" не существует в репозитории \"%s\""
#: repo.go:508 repo.go:580
msgid "Removed %d mirrors from repo \"%s\"\n"
msgstr "Удалены зеркала %d из репозитория \"%s\"\n"
#: repo.go:520
msgid "Remove all mirrors from the repository"
msgstr "Удалить все зеркала из репозитория"
#: repo.go:602
msgid "Name of the new repo" msgid "Name of the new repo"
msgstr "Название нового репозитория" msgstr "Название нового репозитория"
#: repo.go:252 #: repo.go:608
msgid "URL of the new repo" msgid "URL of the new repo"
msgstr "URL-адрес нового репозитория" msgstr "URL-адрес нового репозитория"
#: repo.go:276 #: repo.go:632
msgid "Name of the repo to be deleted" msgid "Name of the repo to be deleted"
msgstr "Название репозитория удалён" msgstr "Название репозитория удалён"
@@ -614,9 +715,6 @@ msgstr "Здесь нечего делать."
#~ msgid "Installing build dependencies" #~ msgid "Installing build dependencies"
#~ msgstr "Установка зависимостей сборки" #~ msgstr "Установка зависимостей сборки"
#~ msgid "Would you like to remove the build dependencies?"
#~ msgstr "Хотели бы вы удалить зависимости сборки?"
#~ msgid "Error installing native packages" #~ msgid "Error installing native packages"
#~ msgstr "Ошибка при установке нативных пакетов" #~ msgstr "Ошибка при установке нативных пакетов"
@@ -633,9 +731,6 @@ msgstr "Здесь нечего делать."
#~ msgid "Unable to detect user config directory" #~ msgid "Unable to detect user config directory"
#~ msgstr "Не удалось обнаружить каталог конфигурации пользователя" #~ msgstr "Не удалось обнаружить каталог конфигурации пользователя"
#~ msgid "Unable to create ALR config file"
#~ msgstr "Не удалось создать конфигурационный файл ALR"
#~ msgid "Error encoding default configuration" #~ msgid "Error encoding default configuration"
#~ msgstr "Ошибка кодирования конфигурации по умолчанию" #~ msgstr "Ошибка кодирования конфигурации по умолчанию"

View File

@@ -17,12 +17,9 @@
package utils package utils
import ( import (
"errors"
"os" "os"
"os/exec" "os/exec"
"os/user" "os/user"
"strconv"
"syscall"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
@@ -32,115 +29,23 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants" "gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
) )
func GetUidGidAlrUserString() (string, string, error) { // IsNotRoot проверяет, что текущий пользователь не является root
u, err := user.Lookup("alr")
if err != nil {
return "", "", err
}
return u.Uid, u.Gid, nil
}
func GetUidGidAlrUser() (int, int, error) {
strUid, strGid, err := GetUidGidAlrUserString()
if err != nil {
return 0, 0, err
}
uid, err := strconv.Atoi(strUid)
if err != nil {
return 0, 0, err
}
gid, err := strconv.Atoi(strGid)
if err != nil {
return 0, 0, err
}
return uid, gid, nil
}
func DropCapsToAlrUser() error {
uid, gid, err := GetUidGidAlrUser()
if err != nil {
return err
}
err = syscall.Setgid(gid)
if err != nil {
return err
}
err = syscall.Setuid(uid)
if err != nil {
return err
}
return EnsureIsAlrUser()
}
func ExitIfCantDropGidToAlr() cli.ExitCoder {
_, gid, err := GetUidGidAlrUser()
if err != nil {
return cliutils.FormatCliExit("cannot get gid alr", err)
}
err = syscall.Setgid(gid)
if err != nil {
return cliutils.FormatCliExit("cannot get setgid alr", err)
}
return nil
}
// ExitIfCantDropCapsToAlrUser attempts to drop capabilities to the already
// running user. Returns a cli.ExitCoder with an error if the operation fails.
// See also [ExitIfCantDropCapsToAlrUserNoPrivs] for a version that also applies
// no-new-privs.
func ExitIfCantDropCapsToAlrUser() cli.ExitCoder {
err := DropCapsToAlrUser()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error on dropping capabilities"), err)
}
return nil
}
func ExitIfCantSetNoNewPrivs() cli.ExitCoder {
if err := NoNewPrivs(); err != nil {
return cliutils.FormatCliExit("error on NoNewPrivs", err)
}
return nil
}
// ExitIfCantDropCapsToAlrUserNoPrivs combines [ExitIfCantDropCapsToAlrUser] with [ExitIfCantSetNoNewPrivs]
func ExitIfCantDropCapsToAlrUserNoPrivs() cli.ExitCoder {
if err := ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
if err := ExitIfCantSetNoNewPrivs(); err != nil {
return err
}
return nil
}
func IsNotRoot() bool { func IsNotRoot() bool {
return os.Getuid() != 0 return os.Getuid() != 0
} }
func EnsureIsAlrUser() error { // EnuseIsPrivilegedGroupMember проверяет, что пользователь является членом привилегированной группы (wheel)
uid, gid, err := GetUidGidAlrUser()
if err != nil {
return err
}
newUid := syscall.Getuid()
if newUid != uid {
return errors.New("new uid don't matches requested")
}
newGid := syscall.Getgid()
if newGid != gid {
return errors.New("new gid don't matches requested")
}
return nil
}
func EnuseIsPrivilegedGroupMember() error { func EnuseIsPrivilegedGroupMember() error {
// В CI пропускаем проверку группы wheel
if os.Getenv("CI") == "true" {
return nil
}
// Если пользователь root, пропускаем проверку
if os.Geteuid() == 0 {
return nil
}
currentUser, err := user.Current() currentUser, err := user.Current()
if err != nil { if err != nil {
return err return err
@@ -164,26 +69,6 @@ func EnuseIsPrivilegedGroupMember() error {
return cliutils.FormatCliExit(gotext.Get("You need to be a %s member to perform this action", constants.PrivilegedGroup), nil) return cliutils.FormatCliExit(gotext.Get("You need to be a %s member to perform this action", constants.PrivilegedGroup), nil)
} }
func EscalateToRootGid() error {
return syscall.Setgid(0)
}
func EscalateToRootUid() error {
return syscall.Setuid(0)
}
func EscalateToRoot() error {
err := EscalateToRootUid()
if err != nil {
return err
}
err = EscalateToRootGid()
if err != nil {
return err
}
return nil
}
func RootNeededAction(f cli.ActionFunc) cli.ActionFunc { func RootNeededAction(f cli.ActionFunc) cli.ActionFunc {
return func(ctx *cli.Context) error { return func(ctx *cli.Context) error {
deps, err := appbuilder. deps, err := appbuilder.

View File

@@ -16,8 +16,78 @@
package utils package utils
import "golang.org/x/sys/unix" import (
"os"
"os/exec"
"strings"
"golang.org/x/sys/unix"
)
func NoNewPrivs() error { func NoNewPrivs() error {
return unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) return unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
} }
// EnsureTempDirWithRootOwner создает каталог в /tmp/alr с правами для группы wheel
// Все каталоги в /tmp/alr принадлежат root:wheel с правами 775
// Для других каталогов использует стандартные права
func EnsureTempDirWithRootOwner(path string, mode os.FileMode) error {
if strings.HasPrefix(path, "/tmp/alr") {
// Сначала создаем директорию обычным способом
err := os.MkdirAll(path, mode)
if err != nil {
return err
}
// В CI или если мы уже root, не нужно использовать sudo
isRoot := os.Geteuid() == 0
isCI := os.Getenv("CI") == "true"
// В CI создаем директории с обычными правами
if isCI {
// В CI не используем группу wheel и не меняем права
// Устанавливаем базовые права 777 для временных каталогов
chmodCmd := exec.Command("chmod", "777", path)
chmodCmd.Run() // Игнорируем ошибки
return nil
}
// Для обычной работы устанавливаем права и группу wheel
permissions := "2775"
group := "wheel"
var chmodCmd, chownCmd *exec.Cmd
if isRoot {
// Выполняем команды напрямую без sudo
chmodCmd = exec.Command("chmod", permissions, path)
chownCmd = exec.Command("chown", "root:"+group, path)
} else {
// Используем sudo для обычных пользователей
chmodCmd = exec.Command("sudo", "chmod", permissions, path)
chownCmd = exec.Command("sudo", "chown", "root:"+group, path)
}
// Устанавливаем права с setgid битом
err = chmodCmd.Run()
if err != nil {
// Для root игнорируем ошибки, если группа wheel не существует
if !isRoot {
return err
}
}
// Устанавливаем владельца root:wheel
err = chownCmd.Run()
if err != nil {
// Для root игнорируем ошибки, если группа wheel не существует
if !isRoot {
return err
}
}
return nil
}
// Для остальных каталогов обычное создание
return os.MkdirAll(path, mode)
}

41
list.go
View File

@@ -24,6 +24,7 @@ import (
"log/slog" "log/slog"
"os" "os"
"slices" "slices"
"strings"
"text/template" "text/template"
"github.com/leonelquinteros/gotext" "github.com/leonelquinteros/gotext"
@@ -33,7 +34,7 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
) )
@@ -58,9 +59,6 @@ func ListCmd() *cli.Command {
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
return err
}
ctx := c.Context ctx := c.Context
@@ -126,7 +124,12 @@ func ListCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Error getting packages"), err) return cliutils.FormatCliExit(gotext.Get("Error getting packages"), err)
} }
installedAlrPackages := map[string]string{} type verInfo struct {
Version string
Release int
}
installedAlrPackages := map[string]verInfo{}
if c.Bool("installed") { if c.Bool("installed") {
mgr := manager.Detect() mgr := manager.Detect()
if mgr == nil { if mgr == nil {
@@ -144,40 +147,50 @@ func ListCmd() *cli.Command {
if matches != nil { if matches != nil {
packageName := matches[build.RegexpALRPackageName.SubexpIndex("package")] packageName := matches[build.RegexpALRPackageName.SubexpIndex("package")]
repoName := matches[build.RegexpALRPackageName.SubexpIndex("repo")] repoName := matches[build.RegexpALRPackageName.SubexpIndex("repo")]
installedAlrPackages[fmt.Sprintf("%s/%s", repoName, packageName)] = version
verInfo := verInfo{
Version: version,
Release: 0,
}
if i := strings.LastIndex(version, "-"); i != -1 {
verInfo.Version = version[:i]
verInfo.Release, err = overrides.ParseReleasePlatformSpecific(version[i+1:], info)
if err != nil {
slog.Error(gotext.Get("Failed to parse release"), "err", err)
return cli.Exit(err, 1)
}
}
installedAlrPackages[fmt.Sprintf("%s/%s", repoName, packageName)] = verInfo
} }
} }
} }
for _, pkg := range result { for _, pkg := range result {
if err != nil {
return cli.Exit(err, 1)
}
if slices.Contains(cfg.IgnorePkgUpdates(), pkg.Name) { if slices.Contains(cfg.IgnorePkgUpdates(), pkg.Name) {
continue continue
} }
type packageInfo struct { type packageInfo struct {
Package *alrsh.Package Package *alrsh.Package
Version string
} }
pkgInfo := &packageInfo{} pkgInfo := &packageInfo{}
pkgInfo.Package = &pkg pkgInfo.Package = &pkg
pkgInfo.Version = pkg.Version
if c.Bool("installed") { if c.Bool("installed") {
instVersion, ok := installedAlrPackages[fmt.Sprintf("%s/%s", pkg.Repository, pkg.Name)] instVersion, ok := installedAlrPackages[fmt.Sprintf("%s/%s", pkg.Repository, pkg.Name)]
if !ok { if !ok {
continue continue
} else { } else {
pkgInfo.Version = instVersion pkg.Version = instVersion.Version
pkg.Release = instVersion.Release
} }
} }
format := c.String("format") format := c.String("format")
if format == "" { if format == "" {
format = "{{.Package.Repository}}/{{.Package.Name}} {{.Version}}\n" format = "{{.Package.Repository}}/{{.Package.Name}} {{.Package.Version}}-{{.Package.Release}}\n"
} }
tmpl, err := template.New("format").Parse(format) tmpl, err := template.New("format").Parse(format)
if err != nil { if err != nil {

View File

@@ -83,10 +83,11 @@ func GetApp() *cli.App {
VersionCmd(), VersionCmd(),
SearchCmd(), SearchCmd(),
RepoCmd(), RepoCmd(),
ConfigCmd(),
// Internal commands // Internal commands
InternalBuildCmd(), InternalBuildCmd(),
InternalInstallCmd(), InternalInstallCmd(),
InternalMountCmd(), InternalReposCmd(),
}, },
Before: func(c *cli.Context) error { Before: func(c *cli.Context) error {
if trimmed := strings.TrimSpace(c.String("pm-args")); trimmed != "" { if trimmed := strings.TrimSpace(c.String("pm-args")); trimmed != "" {

View File

@@ -68,10 +68,12 @@ func (o *OverridableField[T]) Resolve(overrides []string) {
for _, override := range overrides { for _, override := range overrides {
if v, ok := o.Has(override); ok { if v, ok := o.Has(override); ok {
o.SetResolved(v) o.SetResolved(v)
return
} }
} }
} }
// Database serialization (JSON)
func (f *OverridableField[T]) ToDB() ([]byte, error) { func (f *OverridableField[T]) ToDB() ([]byte, error) {
var data map[string]T var data map[string]T
@@ -103,6 +105,7 @@ func (f *OverridableField[T]) FromDB(data []byte) error {
return nil return nil
} }
// Gob serialization
type overridableFieldGobPayload[T any] struct { type overridableFieldGobPayload[T any] struct {
Data map[string]T Data map[string]T
Resolved T Resolved T
@@ -136,6 +139,48 @@ func (f *OverridableField[T]) GobDecode(data []byte) error {
return nil return nil
} }
type overridableFieldJSONPayload[T any] struct {
Resolved *T `json:"resolved,omitempty,omitzero"`
Data map[string]T `json:"overrides,omitempty,omitzero"`
}
func (f OverridableField[T]) MarshalJSON() ([]byte, error) {
data := make(map[string]T)
for k, v := range f.data {
if k == "" {
data["default"] = v
} else {
data[k] = v
}
}
payload := overridableFieldJSONPayload[T]{
Data: data,
Resolved: &f.resolved,
}
return json.Marshal(payload)
}
func (f *OverridableField[T]) UnmarshalJSON(data []byte) error {
var payload overridableFieldJSONPayload[T]
if err := json.Unmarshal(data, &payload); err != nil {
return err
}
if payload.Data == nil {
payload.Data = make(map[string]T)
}
f.data = payload.Data
if payload.Resolved != nil {
f.resolved = *payload.Resolved
}
return nil
}
func OverridableFromMap[T any](data map[string]T) OverridableField[T] { func OverridableFromMap[T any](data map[string]T) OverridableField[T] {
if data == nil { if data == nil {
data = make(map[string]T) data = make(map[string]T)

View File

@@ -14,9 +14,12 @@
// You should have received a copy of the GNU General Public License // You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
//go:generate bash -c "go run ../../generators/alrsh-package && cd ../.. && make update-license"
package alrsh package alrsh
import ( import (
"encoding/json"
"fmt" "fmt"
"reflect" "reflect"
"strings" "strings"
@@ -39,38 +42,38 @@ func ParseNames(dec *decoder.Decoder) (*PackageNames, error) {
} }
type Package struct { type Package struct {
Repository string `xorm:"pk 'repository'"` Repository string `xorm:"pk 'repository'" json:"repository"`
Name string `xorm:"pk 'name'"` Name string `xorm:"pk 'name'" json:"name"`
BasePkgName string `xorm:"notnull 'basepkg_name'"` BasePkgName string `xorm:"notnull 'basepkg_name'" json:"basepkg_name"`
Version string `sh:"version" xorm:"notnull 'version'"` Version string `sh:"version" xorm:"notnull 'version'" json:"version"`
Release int `sh:"release" xorm:"notnull 'release'"` Release int `sh:"release" xorm:"notnull 'release'" json:"release"`
Epoch uint `sh:"epoch" xorm:"'epoch'"` Epoch uint `sh:"epoch" xorm:"'epoch'" json:"epoch"`
Architectures []string `sh:"architectures" xorm:"json 'architectures'"` Architectures []string `sh:"architectures" xorm:"json 'architectures'" json:"architectures"`
Licenses []string `sh:"license" xorm:"json 'licenses'"` Licenses []string `sh:"license" xorm:"json 'licenses'" json:"license"`
Provides []string `sh:"provides" xorm:"json 'provides'"` Provides []string `sh:"provides" xorm:"json 'provides'" json:"provides"`
Conflicts []string `sh:"conflicts" xorm:"json 'conflicts'"` Conflicts []string `sh:"conflicts" xorm:"json 'conflicts'" json:"conflicts"`
Replaces []string `sh:"replaces" xorm:"json 'replaces'"` Replaces []string `sh:"replaces" xorm:"json 'replaces'" json:"replaces"`
Summary OverridableField[string] `sh:"summary" xorm:"'summary'"` Summary OverridableField[string] `sh:"summary" xorm:"'summary'" json:"summary"`
Description OverridableField[string] `sh:"desc" xorm:"'description'"` Description OverridableField[string] `sh:"desc" xorm:"'description'" json:"description"`
Group OverridableField[string] `sh:"group" xorm:"'group_name'"` Group OverridableField[string] `sh:"group" xorm:"'group_name'" json:"group"`
Homepage OverridableField[string] `sh:"homepage" xorm:"'homepage'"` Homepage OverridableField[string] `sh:"homepage" xorm:"'homepage'" json:"homepage"`
Maintainer OverridableField[string] `sh:"maintainer" xorm:"'maintainer'"` Maintainer OverridableField[string] `sh:"maintainer" xorm:"'maintainer'" json:"maintainer"`
Depends OverridableField[[]string] `sh:"deps" xorm:"'depends'"` Depends OverridableField[[]string] `sh:"deps" xorm:"'depends'" json:"deps"`
BuildDepends OverridableField[[]string] `sh:"build_deps" xorm:"'builddepends'"` BuildDepends OverridableField[[]string] `sh:"build_deps" xorm:"'builddepends'" json:"build_deps"`
OptDepends OverridableField[[]string] `sh:"opt_deps" xorm:"'optdepends'"` OptDepends OverridableField[[]string] `sh:"opt_deps" xorm:"'optdepends'" json:"opt_deps,omitempty"`
Sources OverridableField[[]string] `sh:"sources" xorm:"-"` Sources OverridableField[[]string] `sh:"sources" xorm:"-" json:"sources"`
Checksums OverridableField[[]string] `sh:"checksums" xorm:"-"` Checksums OverridableField[[]string] `sh:"checksums" xorm:"-" json:"checksums,omitempty"`
Backup OverridableField[[]string] `sh:"backup" xorm:"-"` Backup OverridableField[[]string] `sh:"backup" xorm:"-" json:"backup"`
Scripts OverridableField[Scripts] `sh:"scripts" xorm:"-"` Scripts OverridableField[Scripts] `sh:"scripts" xorm:"-" json:"scripts,omitempty"`
AutoReq OverridableField[[]string] `sh:"auto_req" xorm:"-"` AutoReq OverridableField[[]string] `sh:"auto_req" xorm:"-" json:"auto_req"`
AutoProv OverridableField[[]string] `sh:"auto_prov" xorm:"-"` AutoProv OverridableField[[]string] `sh:"auto_prov" xorm:"-" json:"auto_prov"`
AutoReqSkipList OverridableField[[]string] `sh:"auto_req_skiplist" xorm:"-"` AutoReqSkipList OverridableField[[]string] `sh:"auto_req_skiplist" xorm:"-" json:"auto_req_skiplist,omitempty"`
AutoProvSkipList OverridableField[[]string] `sh:"auto_prov_skiplist" xorm:"-"` AutoProvSkipList OverridableField[[]string] `sh:"auto_prov_skiplist" xorm:"-" json:"auto_prov_skiplist,omitempty"`
FireJailed OverridableField[bool] `sh:"firejailed" xorm:"-"` FireJailed OverridableField[bool] `sh:"firejailed" xorm:"-" json:"firejailed"`
FireJailProfiles OverridableField[map[string]string] `sh:"firejail_profiles" xorm:"-"` FireJailProfiles OverridableField[map[string]string] `sh:"firejail_profiles" xorm:"-" json:"firejail_profiles,omitempty"`
} }
type Scripts struct { type Scripts struct {
@@ -84,25 +87,70 @@ type Scripts struct {
PostTrans string `sh:"posttrans"` PostTrans string `sh:"posttrans"`
} }
func ResolvePackage(p *Package, overrides []string) { func (p Package) MarshalJSONWithOptions(includeOverrides bool) ([]byte, error) {
val := reflect.ValueOf(p).Elem() // Сначала сериализуем обычным способом для получения базовой структуры
typ := val.Type() type PackageAlias Package
baseData, err := json.Marshal(PackageAlias(p))
if err != nil {
return nil, err
}
for i := range val.NumField() { // Десериализуем в map для модификации
field := val.Field(i) var result map[string]json.RawMessage
fieldType := typ.Field(i) if err := json.Unmarshal(baseData, &result); err != nil {
return nil, err
}
if !field.CanInterface() { // Теперь заменяем OverridableField поля
v := reflect.ValueOf(p)
t := reflect.TypeOf(p)
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := t.Field(i)
jsonTag := fieldType.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue continue
} }
if field.Kind() == reflect.Struct && strings.HasPrefix(fieldType.Type.String(), "alrsh.OverridableField") { fieldName := jsonTag
of := field.Addr().Interface() if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
if res, ok := of.(interface { fieldName = jsonTag[:commaIdx]
Resolve([]string) }
}); ok {
res.Resolve(overrides) if field.Type().Name() == "OverridableField" ||
(field.Type().Kind() == reflect.Struct &&
strings.Contains(field.Type().String(), "OverridableField")) {
fieldPtr := field.Addr()
resolvedMethod := fieldPtr.MethodByName("Resolved")
if resolvedMethod.IsValid() {
resolved := resolvedMethod.Call(nil)[0]
fieldData := map[string]interface{}{
"resolved": resolved.Interface(),
}
if includeOverrides {
allMethod := field.MethodByName("All")
if allMethod.IsValid() {
overrides := allMethod.Call(nil)[0]
if !overrides.IsNil() && overrides.Len() > 0 {
fieldData["overrides"] = overrides.Interface()
}
}
}
fieldJSON, err := json.Marshal(fieldData)
if err != nil {
return nil, err
}
result[fieldName] = json.RawMessage(fieldJSON)
} }
} }
} }
return json.Marshal(result)
} }

105
pkg/alrsh/package_gen.go Normal file
View File

@@ -0,0 +1,105 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
// DO NOT EDIT MANUALLY. This file is generated.
package alrsh
type packageResolved struct {
Repository string `json:"repository"`
Name string `json:"name"`
BasePkgName string `json:"basepkg_name"`
Version string `json:"version"`
Release int `json:"release"`
Epoch uint `json:"epoch"`
Architectures []string `json:"architectures"`
Licenses []string `json:"license"`
Provides []string `json:"provides"`
Conflicts []string `json:"conflicts"`
Replaces []string `json:"replaces"`
Summary string `json:"summary"`
Description string `json:"description"`
Group string `json:"group"`
Homepage string `json:"homepage"`
Maintainer string `json:"maintainer"`
Depends []string `json:"deps"`
BuildDepends []string `json:"build_deps"`
OptDepends []string `json:"opt_deps,omitempty"`
Sources []string `json:"sources"`
Checksums []string `json:"checksums,omitempty"`
Backup []string `json:"backup"`
Scripts Scripts `json:"scripts,omitempty"`
AutoReq []string `json:"auto_req"`
AutoProv []string `json:"auto_prov"`
AutoReqSkipList []string `json:"auto_req_skiplist,omitempty"`
AutoProvSkipList []string `json:"auto_prov_skiplist,omitempty"`
FireJailed bool `json:"firejailed"`
FireJailProfiles map[string]string `json:"firejail_profiles,omitempty"`
}
func PackageToResolved(src *Package) packageResolved {
return packageResolved{
Repository: src.Repository,
Name: src.Name,
BasePkgName: src.BasePkgName,
Version: src.Version,
Release: src.Release,
Epoch: src.Epoch,
Architectures: src.Architectures,
Licenses: src.Licenses,
Provides: src.Provides,
Conflicts: src.Conflicts,
Replaces: src.Replaces,
Summary: src.Summary.Resolved(),
Description: src.Description.Resolved(),
Group: src.Group.Resolved(),
Homepage: src.Homepage.Resolved(),
Maintainer: src.Maintainer.Resolved(),
Depends: src.Depends.Resolved(),
BuildDepends: src.BuildDepends.Resolved(),
OptDepends: src.OptDepends.Resolved(),
Sources: src.Sources.Resolved(),
Checksums: src.Checksums.Resolved(),
Backup: src.Backup.Resolved(),
Scripts: src.Scripts.Resolved(),
AutoReq: src.AutoReq.Resolved(),
AutoProv: src.AutoProv.Resolved(),
AutoReqSkipList: src.AutoReqSkipList.Resolved(),
AutoProvSkipList: src.AutoProvSkipList.Resolved(),
FireJailed: src.FireJailed.Resolved(),
FireJailProfiles: src.FireJailProfiles.Resolved(),
}
}
func ResolvePackage(pkg *Package, overrides []string) {
pkg.Summary.Resolve(overrides)
pkg.Description.Resolve(overrides)
pkg.Group.Resolve(overrides)
pkg.Homepage.Resolve(overrides)
pkg.Maintainer.Resolve(overrides)
pkg.Depends.Resolve(overrides)
pkg.BuildDepends.Resolve(overrides)
pkg.OptDepends.Resolve(overrides)
pkg.Sources.Resolve(overrides)
pkg.Checksums.Resolve(overrides)
pkg.Backup.Resolve(overrides)
pkg.Scripts.Resolve(overrides)
pkg.AutoReq.Resolve(overrides)
pkg.AutoProv.Resolve(overrides)
pkg.AutoReqSkipList.Resolve(overrides)
pkg.AutoProvSkipList.Resolve(overrides)
pkg.FireJailed.Resolve(overrides)
pkg.FireJailProfiles.Resolve(overrides)
}

37
pkg/alrsh/view.go Normal file
View File

@@ -0,0 +1,37 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package alrsh
import "encoding/json"
type PackageView struct {
pkg Package
Resolved bool
}
func NewPackageView(v Package) PackageView {
return PackageView{pkg: v}
}
func (p PackageView) MarshalJSON() ([]byte, error) {
if p.Resolved {
return json.Marshal(PackageToResolved(&p.pkg))
} else {
return json.Marshal(p.pkg)
}
}

View File

@@ -55,7 +55,7 @@ var (
// Массив доступных загрузчиков в порядке их проверки // Массив доступных загрузчиков в порядке их проверки
var Downloaders = []Downloader{ var Downloaders = []Downloader{
GitDownloader{}, &GitDownloader{},
TorrentDownloader{}, TorrentDownloader{},
FileDownloader{}, FileDownloader{},
} }
@@ -172,15 +172,10 @@ func Download(ctx context.Context, opts Options) (err error) {
"downloader", d.Name(), "downloader", d.Name(),
) )
updated, err = d.Update(Options{ newOpts := opts
Hash: opts.Hash, newOpts.Destination = cacheDir
HashAlgorithm: opts.HashAlgorithm,
Name: opts.Name, updated, err = d.Update(newOpts)
URL: opts.URL,
Destination: cacheDir,
Progress: opts.Progress,
LocalDir: opts.LocalDir,
})
if err != nil { if err != nil {
return err return err
} }
@@ -226,15 +221,10 @@ func Download(ctx context.Context, opts Options) (err error) {
return err return err
} }
t, name, err := d.Download(ctx, Options{ newOpts := opts
Hash: opts.Hash, newOpts.Destination = cacheDir
HashAlgorithm: opts.HashAlgorithm,
Name: opts.Name, t, name, err := d.Download(ctx, newOpts)
URL: opts.URL,
Destination: cacheDir,
Progress: opts.Progress,
LocalDir: opts.LocalDir,
})
if err != nil { if err != nil {
return err return err
} }
@@ -290,14 +280,14 @@ func handleCache(cacheDir, dest, name string, t Type) (bool, error) {
cd.Close() cd.Close()
if slices.Contains(names, name) { if slices.Contains(names, name) {
err = os.Link(filepath.Join(cacheDir, name), dest) err = linkOrCopy(filepath.Join(cacheDir, name), dest)
if err != nil { if err != nil {
return false, err return false, err
} }
return true, nil return true, nil
} }
case TypeDir: case TypeDir:
err := linkDir(cacheDir, dest) err := linkOrCopyDir(cacheDir, dest)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -306,8 +296,40 @@ func handleCache(cacheDir, dest, name string, t Type) (bool, error) {
return false, nil return false, nil
} }
// Функция linkDir рекурсивно создает жесткие ссылки для файлов из каталога src в каталог dest // linkOrCopy пытается создать жесткую ссылку, а если не получается - копирует файл
func linkDir(src, dest string) error { func linkOrCopy(src, dest string) error {
err := os.Link(src, dest)
if err != nil {
// Если не удалось создать ссылку, копируем файл
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
destFile, err := os.Create(dest)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, srcFile)
if err != nil {
return err
}
// Копируем права доступа
srcInfo, err := srcFile.Stat()
if err != nil {
return err
}
return os.Chmod(dest, srcInfo.Mode())
}
return nil
}
// linkOrCopyDir рекурсивно создает жесткие ссылки или копирует файлы из каталога src в каталог dest
func linkOrCopyDir(src, dest string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
@@ -327,7 +349,7 @@ func linkDir(src, dest string) error {
return os.MkdirAll(newPath, info.Mode()) return os.MkdirAll(newPath, info.Mode())
} }
return os.Link(path, newPath) return linkOrCopy(path, newPath)
}) })
} }

View File

@@ -32,8 +32,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dl" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dlcache" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
) )
type TestALRConfig struct{} type TestALRConfig struct{}
@@ -155,7 +155,7 @@ func TestDownloadFileWithCache(t *testing.T) {
CacheDisabled: false, CacheDisabled: false,
URL: server.URL + "/file", URL: server.URL + "/file",
Destination: tmpdir, Destination: tmpdir,
DlCache: dlcache.New(cfg), DlCache: dlcache.New(cfg.GetPaths().CacheDir),
} }
outputFile := path.Join(tmpdir, "file") outputFile := path.Join(tmpdir, "file")

View File

@@ -108,7 +108,7 @@ func (FileDownloader) Download(ctx context.Context, opts Options) (Type, string,
} }
defer r.Close() defer r.Close()
opts.PostprocDisabled = archive == "false" postprocDisabled := opts.PostprocDisabled || archive == "false"
path := filepath.Join(opts.Destination, name) path := filepath.Join(opts.Destination, name)
fl, err := os.Create(path) fl, err := os.Create(path)
@@ -154,7 +154,7 @@ func (FileDownloader) Download(ctx context.Context, opts Options) (Type, string,
} }
// Проверка необходимости постобработки // Проверка необходимости постобработки
if opts.PostprocDisabled { if postprocDisabled {
return TypeFile, name, nil return TypeFile, name, nil
} }

View File

@@ -22,6 +22,7 @@ package dl
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net/url" "net/url"
"path" "path"
"strconv" "strconv"
@@ -48,7 +49,7 @@ func (GitDownloader) MatchURL(u string) bool {
// Download uses git to clone the repository from the specified URL. // Download uses git to clone the repository from the specified URL.
// It allows specifying the revision, depth and recursion options // It allows specifying the revision, depth and recursion options
// via query string // via query string
func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string, error) { func (d *GitDownloader) Download(ctx context.Context, opts Options) (Type, string, error) {
u, err := url.Parse(opts.URL) u, err := url.Parse(opts.URL)
if err != nil { if err != nil {
return 0, "", err return 0, "", err
@@ -60,6 +61,9 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
rev := query.Get("~rev") rev := query.Get("~rev")
query.Del("~rev") query.Del("~rev")
// Right now, this only affects the return value of name,
// which will be used by dl_cache.
// It seems wrong, but for now it's better to leave it as it is.
name := query.Get("~name") name := query.Get("~name")
query.Del("~name") query.Del("~name")
@@ -121,6 +125,11 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
} }
} }
err = VerifyHashFromLocal("", opts)
if err != nil {
return 0, "", err
}
if name == "" { if name == "" {
name = strings.TrimSuffix(path.Base(u.Path), ".git") name = strings.TrimSuffix(path.Base(u.Path), ".git")
} }
@@ -133,7 +142,7 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
// and recursion options via query string. It returns // and recursion options via query string. It returns
// true if update was successful and false if the // true if update was successful and false if the
// repository is already up-to-date // repository is already up-to-date
func (GitDownloader) Update(opts Options) (bool, error) { func (d *GitDownloader) Update(opts Options) (bool, error) {
u, err := url.Parse(opts.URL) u, err := url.Parse(opts.URL)
if err != nil { if err != nil {
return false, err return false, err
@@ -141,6 +150,7 @@ func (GitDownloader) Update(opts Options) (bool, error) {
u.Scheme = strings.TrimPrefix(u.Scheme, "git+") u.Scheme = strings.TrimPrefix(u.Scheme, "git+")
query := u.Query() query := u.Query()
rev := query.Get("~rev")
query.Del("~rev") query.Del("~rev")
depthStr := query.Get("~depth") depthStr := query.Get("~depth")
@@ -169,32 +179,75 @@ func (GitDownloader) Update(opts Options) (bool, error) {
} }
} }
po := &git.PullOptions{ // First, we do a fetch to get all the revisions.
Depth: depth, fo := &git.FetchOptions{
Progress: opts.Progress, Depth: depth,
RecurseSubmodules: git.NoRecurseSubmodules, Progress: opts.Progress,
}
if recursive == "true" {
po.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth
} }
m, err := getManifest(opts.Destination) m, err := getManifest(opts.Destination)
manifestOK := err == nil manifestOK := err == nil
err = w.Pull(po) err = r.Fetch(fo)
if errors.Is(err, git.NoErrAlreadyUpToDate) { if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return false, nil return false, err
} else if err != nil { }
// If a revision is specified, switch to it.
if rev != "" {
// We are trying to find the revision as a hash of the commit
hash, err := r.ResolveRevision(plumbing.Revision(rev))
if err != nil {
return false, fmt.Errorf("failed to resolve revision %s: %w", rev, err)
}
err = w.Checkout(&git.CheckoutOptions{
Hash: *hash,
})
if err != nil {
return false, fmt.Errorf("failed to checkout revision %s: %w", rev, err)
}
if recursive == "true" {
submodules, err := w.Submodules()
if err == nil {
err = submodules.Update(&git.SubmoduleUpdateOptions{
Init: true,
})
if err != nil {
return false, fmt.Errorf("failed to update submodules %s: %w", rev, err)
}
}
}
} else {
// If the revision is not specified, we do a regular pull.
po := &git.PullOptions{
Depth: depth,
Progress: opts.Progress,
RecurseSubmodules: git.NoRecurseSubmodules,
}
if recursive == "true" {
po.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth
}
err = w.Pull(po)
if err != nil {
if errors.Is(err, git.NoErrAlreadyUpToDate) {
return false, nil
}
return false, err
}
}
err = VerifyHashFromLocal("", opts)
if err != nil {
return false, err return false, err
} }
if manifestOK { if manifestOK {
err = writeManifest(opts.Destination, m) err = writeManifest(opts.Destination, m)
if err != nil {
return true, err
}
} }
return true, nil return true, err
} }

183
pkg/dl/git_test.go Normal file
View File

@@ -0,0 +1,183 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package dl_test
import (
"context"
"encoding/hex"
"os"
"os/exec"
"testing"
"github.com/stretchr/testify/assert"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl"
)
func TestGitDownloaderMatchUrl(t *testing.T) {
d := dl.GitDownloader{}
assert.True(t, d.MatchURL("git+https://example.com/org/project.git"))
assert.False(t, d.MatchURL("https://example.com/org/project.git"))
}
func TestGitDownloaderDownload(t *testing.T) {
d := dl.GitDownloader{}
createTempDir := func(t *testing.T, name string) string {
t.Helper()
dir, err := os.MkdirTemp("", "test-"+name)
assert.NoError(t, err)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
return dir
}
t.Run("simple", func(t *testing.T) {
dest := createTempDir(t, "simple")
dlType, name, err := d.Download(context.Background(), dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git",
Destination: dest,
})
assert.NoError(t, err)
assert.Equal(t, dl.TypeDir, dlType)
assert.Equal(t, "repo-for-tests", name)
})
t.Run("with hash", func(t *testing.T) {
dest := createTempDir(t, "with-hash")
hsh, err := hex.DecodeString("33c912b855352663550003ca6b948ae3df1f38e2c036f5a85775df5967e143bf")
assert.NoError(t, err)
dlType, name, err := d.Download(context.Background(), dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git?~rev=init&~name=test",
Destination: dest,
Hash: hsh,
HashAlgorithm: "sha256",
})
assert.NoError(t, err)
assert.Equal(t, dl.TypeDir, dlType)
assert.Equal(t, "test", name)
})
t.Run("with hash (checksum mismatch)", func(t *testing.T) {
dest := createTempDir(t, "with-hash-checksum-mismatch")
hsh, err := hex.DecodeString("33c912b855352663550003ca6b948ae3df1f38e2c036f5a85775df5967e143bf")
assert.NoError(t, err)
_, _, err = d.Download(context.Background(), dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git",
Destination: dest,
Hash: hsh,
HashAlgorithm: "sha256",
})
assert.ErrorIs(t, err, dl.ErrChecksumMismatch)
})
}
func TestGitDownloaderUpdate(t *testing.T) {
d := dl.GitDownloader{}
createTempDir := func(t *testing.T, name string) string {
t.Helper()
dir, err := os.MkdirTemp("", "test-"+name)
assert.NoError(t, err)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
return dir
}
setupOldRepo := func(t *testing.T, dest string) {
t.Helper()
cmd := exec.Command("git", "clone", "https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git", dest)
err := cmd.Run()
assert.NoError(t, err)
cmd = exec.Command("git", "-C", dest, "reset", "--hard", "init")
err = cmd.Run()
assert.NoError(t, err)
}
t.Run("simple", func(t *testing.T) {
dest := createTempDir(t, "update")
setupOldRepo(t, dest)
cmd := exec.Command("git", "-C", dest, "rev-parse", "HEAD")
oldHash, err := cmd.Output()
assert.NoError(t, err)
updated, err := d.Update(dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git",
Destination: dest,
})
assert.NoError(t, err)
assert.True(t, updated)
cmd = exec.Command("git", "-C", dest, "rev-parse", "HEAD")
newHash, err := cmd.Output()
assert.NoError(t, err)
assert.NotEqual(t, string(oldHash), string(newHash), "Repository should be updated")
})
t.Run("with hash", func(t *testing.T) {
dest := createTempDir(t, "update")
setupOldRepo(t, dest)
hsh, err := hex.DecodeString("0dc4f3c68c435d0cd7a5ee960f965815fa9c4ee0571839cdb8f9de56e06f91eb")
assert.NoError(t, err)
updated, err := d.Update(dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git?~rev=test-update-git-downloader",
Destination: dest,
Hash: hsh,
HashAlgorithm: "sha256",
})
assert.NoError(t, err)
assert.True(t, updated)
})
t.Run("with hash (checksum mismatch)", func(t *testing.T) {
dest := createTempDir(t, "update")
setupOldRepo(t, dest)
hsh, err := hex.DecodeString("33c912b855352663550003ca6b948ae3df1f38e2c036f5a85775df5967e143bf")
assert.NoError(t, err)
_, err = d.Update(dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git?~rev=test-update-git-downloader",
Destination: dest,
Hash: hsh,
HashAlgorithm: "sha256",
})
assert.ErrorIs(t, err, dl.ErrChecksumMismatch)
})
}

View File

@@ -71,7 +71,17 @@ func (TorrentDownloader) Download(ctx context.Context, opts Options) (Type, stri
return 0, "", err return 0, "", err
} }
return determineType(opts.Destination) dlType, name, err := determineType(opts.Destination)
if err != nil {
return 0, "", err
}
err = VerifyHashFromLocal(name, opts)
if err != nil {
return 0, "", err
}
return dlType, name, nil
} }
func removeTorrentFiles(path string) error { func removeTorrentFiles(path string) error {

95
pkg/dl/utils.go Normal file
View File

@@ -0,0 +1,95 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package dl
import (
"bytes"
"encoding/hex"
"fmt"
"hash"
"io"
"log/slog"
"os"
"path/filepath"
)
// If the checksum does not match, returns ErrChecksumMismatch
func VerifyHashFromLocal(path string, opts Options) error {
if opts.Hash != nil {
h, err := opts.NewHash()
if err != nil {
return err
}
err = HashLocal(filepath.Join(opts.Destination, path), h)
if err != nil {
return err
}
sum := h.Sum(nil)
slog.Debug("validate checksum", "real", hex.EncodeToString(sum), "expected", hex.EncodeToString(opts.Hash))
if !bytes.Equal(sum, opts.Hash) {
return ErrChecksumMismatch
}
}
return nil
}
func HashLocal(path string, h hash.Hash) error {
info, err := os.Stat(path)
if err != nil {
return err
}
if info.Mode().IsRegular() {
// Single file
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(h, f)
return err
}
if info.IsDir() {
// Walk directory
return filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() && info.Name() == ".git" {
return filepath.SkipDir
}
if !info.Mode().IsRegular() {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(h, f)
return err
})
}
return fmt.Errorf("unsupported file type: %s", path)
}

View File

@@ -32,19 +32,15 @@ type Config interface {
} }
type DownloadCache struct { type DownloadCache struct {
cfg Config cacheDir string
} }
func New(cfg Config) *DownloadCache { func New(cacheDir string) *DownloadCache {
return &DownloadCache{ return &DownloadCache{cacheDir}
cfg,
}
} }
func (dc *DownloadCache) BasePath(ctx context.Context) string { func (dc *DownloadCache) BasePath(ctx context.Context) string {
return filepath.Join( return filepath.Join(dc.cacheDir, "dl")
dc.cfg.GetPaths().CacheDir, "dl",
)
} }
// New creates a new directory with the given ID in the cache. // New creates a new directory with the given ID in the cache.
@@ -65,7 +61,8 @@ func (dc *DownloadCache) New(ctx context.Context, id string) (string, error) {
} }
} }
err = os.MkdirAll(itemPath, 0o755) // Создаем директорию с правильными правами (различается для prod и тестов)
err = createDir(itemPath, 0o2775)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -0,0 +1,37 @@
//go:build !test
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// 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 <http://www.gnu.org/licenses/>.
package dlcache
import (
"os"
"strings"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
)
// createDir создает директорию с правильными правами для production
func createDir(itemPath string, mode os.FileMode) error {
// Используем специальную функцию для создания каталогов с setgid битом только для /tmp/alr
// В остальных случаях используем обычное создание директории
if strings.HasPrefix(itemPath, "/tmp/alr") {
return utils.EnsureTempDirWithRootOwner(itemPath, mode)
} else {
return os.MkdirAll(itemPath, mode)
}
}

View File

@@ -29,7 +29,7 @@ import (
"testing" "testing"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dlcache" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
) )
type TestALRConfig struct { type TestALRConfig struct {
@@ -45,7 +45,7 @@ func (c *TestALRConfig) GetPaths() *config.Paths {
func prepare(t *testing.T) *TestALRConfig { func prepare(t *testing.T) *TestALRConfig {
t.Helper() t.Helper()
dir, err := os.MkdirTemp("/tmp", "alr-dlcache-test.*") dir, err := os.MkdirTemp("", "alr-dlcache-test.*")
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -57,14 +57,14 @@ func prepare(t *testing.T) *TestALRConfig {
func cleanup(t *testing.T, cfg *TestALRConfig) { func cleanup(t *testing.T, cfg *TestALRConfig) {
t.Helper() t.Helper()
os.Remove(cfg.CacheDir) os.RemoveAll(cfg.CacheDir)
} }
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
cfg := prepare(t) cfg := prepare(t)
defer cleanup(t, cfg) defer cleanup(t, cfg)
dc := dlcache.New(cfg) dc := dlcache.New(cfg.GetPaths().CacheDir)
ctx := context.Background() ctx := context.Background()
@@ -82,6 +82,12 @@ func TestNew(t *testing.T) {
fi, err := os.Stat(dir) fi, err := os.Stat(dir)
if err != nil { if err != nil {
t.Errorf("stat: expected no error, got %s", err) t.Errorf("stat: expected no error, got %s", err)
return
}
if fi == nil {
t.Errorf("Expected file info to not be nil")
return
} }
if !fi.IsDir() { if !fi.IsDir() {

Some files were not shown because too many files have changed in this diff Show More