Compare commits
	
		
			41 Commits
		
	
	
		
			85d18880a4
			...
			v0.0.21
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 42f0d5e575 | |||
| 7b9404a058 | |||
| 18e8dc3fbf | |||
| 9c0af83a20 | |||
| 4bd20d84ef | |||
| 8dea5e1e7f | |||
| 86a982478e | |||
| 8bc82cb95c | |||
| 9783ce37de | |||
| b852688ab0 | |||
| 2ff5e6f7b6 | |||
| c9639b7073 | |||
| c1847e1191 | |||
| f2b0f57c12 | |||
| 59cc41e94c | |||
| 75ece6dfcc | |||
| 6af712f1d5 | |||
| bad225c6b1 | |||
| 4b3bf44aaa | |||
| 67b3c40430 | |||
| 4948e6b8fc | |||
| 292125a8ff | |||
| 77055aa2cb | |||
| 737bf68f95 | |||
| 1089e8a3f3 | |||
| aa42ab0607 | |||
| 51fa7ca6fb | |||
| ab41700004 | |||
| 7cb1bc9548 | |||
| 07187da423 | |||
| 802fe2b0b2 | |||
| aa08c04e0c | |||
| f42be105ad | |||
| 1cc408ad7d | |||
| 4899e203bb | |||
| 67a6cb31de | |||
| 5e24940ef8 | |||
| a600feb083 | |||
| 7060e4f551 | |||
| d77ca4c384 | |||
| 6355f25089 | 
| @@ -19,7 +19,7 @@ name: Pre-commit | |||||||
|  |  | ||||||
| on: | on: | ||||||
|   push: |   push: | ||||||
|     branches: [ main ] |     branches: [ master ] | ||||||
|   pull_request: |   pull_request: | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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: | ||||||
| @@ -78,43 +78,57 @@ jobs: | |||||||
|           token: ${{ secrets.GITEAPUBLIC }} |           token: ${{ secrets.GITEAPUBLIC }} | ||||||
|           path: alr-default |           path: alr-default | ||||||
|  |  | ||||||
|       - name: Update version in alr-bin |       - name: Calculate checksum | ||||||
|         run: | |         run: | | ||||||
|           # Замените значения в файле с конфигурацией |           # Вычисляем SHA256 контрольную сумму архива | ||||||
|  |           CHECKSUM=$(sha256sum alr-${{ env.VERSION }}-linux-x86_64.tar.gz | awk '{print $1}') | ||||||
|  |           echo "Archive checksum: $CHECKSUM" | ||||||
|  |           echo "CHECKSUM=$CHECKSUM" >> $GITHUB_ENV | ||||||
|  |  | ||||||
|  |       - name: Update version and checksum in alr-bin | ||||||
|  |         run: | | ||||||
|  |           # Обновляем версию | ||||||
|           sed -i "s/version='[0-9]\+\.[0-9]\+\.[0-9]\+'/version='${{ env.VERSION }}'/g" alr-default/alr-bin/alr.sh |           sed -i "s/version='[0-9]\+\.[0-9]\+\.[0-9]\+'/version='${{ env.VERSION }}'/g" alr-default/alr-bin/alr.sh | ||||||
|           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 |           # Обновляем контрольную сумму | ||||||
| #        run: | |           sed -i "s/checksums=('[^']*')/checksums=('${{ env.CHECKSUM }}')/g" alr-default/alr-bin/alr.sh | ||||||
| #          make install |  | ||||||
| # |  | ||||||
| #          # temporary fix |  | ||||||
| #          groupadd wheel |  | ||||||
| #          usermod -aG wheel root |  | ||||||
|  |  | ||||||
| #      - name: Build packages |       - name: Commit and push changes to alr-default | ||||||
| #        run: | |  | ||||||
| #          SCRIPT_PATH=alr-default/alr-bin/alr.sh |  | ||||||
| #          ALR_DISTRO=altlinux ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH" |  | ||||||
| #          ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH" |  | ||||||
| #          ALR_PKG_FORMAT=deb alr build -s "$SCRIPT_PATH" |  | ||||||
| #          ALR_PKG_FORMAT=archlinux alr build -s "$SCRIPT_PATH" |  | ||||||
|  |  | ||||||
| #      - name: Upload assets |  | ||||||
| #        uses: akkuman/gitea-release-action@v1 |  | ||||||
| #        with: |  | ||||||
| #          body: ${{ steps.changes.outputs.changes }} |  | ||||||
| #          files: |- |  | ||||||
| #            alr-bin+alr-default_${{ env.VERSION }}-1.red80_amd64.deb \ |  | ||||||
| #            alr-bin+alr-default-${{ env.VERSION }}-1-x86_64.pkg.tar.zst \ |  | ||||||
| #            alr-bin+alr-default-${{ env.VERSION }}-1.red80.x86_64.rpm \ |  | ||||||
| #            alr-bin+alr-default-${{ env.VERSION }}-alt1.x86_64.rpm |  | ||||||
|  |  | ||||||
|       - name: Commit changes |  | ||||||
|         run: | |         run: | | ||||||
|           cd alr-default |           cd alr-default | ||||||
|           git config user.name "gitea" |           git config user.name "gitea" | ||||||
|           git config user.email "admin@plemya-x.ru" |           git config user.email "admin@plemya-x.ru" | ||||||
|           git add . |           git add alr-bin/alr.sh | ||||||
|           git commit -m "Обновление версии до ${{ env.VERSION }}" |           git commit -m "Обновление alr-bin до версии ${{ env.VERSION }}" | ||||||
|           git push |           git push | ||||||
|  |  | ||||||
|  |       - name: Install alr | ||||||
|  |         env: | ||||||
|  |           CREATE_SYSTEM_RESOURCES: 0 | ||||||
|  |         run: | | ||||||
|  |           make install | ||||||
|  |  | ||||||
|  |       - name: Prepare directories for ALR | ||||||
|  |         run: | | ||||||
|  |           # Создаём необходимые директории для работы 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 | ||||||
|  |         run: | | ||||||
|  |           SCRIPT_PATH=alr-default/alr-bin/alr.sh | ||||||
|  |           ALR_DISTRO=altlinux ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH" | ||||||
|  |           ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH" | ||||||
|  |           ALR_PKG_FORMAT=deb alr build -s "$SCRIPT_PATH" | ||||||
|  |           ALR_PKG_FORMAT=archlinux alr build -s "$SCRIPT_PATH" | ||||||
|  |  | ||||||
|  |       - name: Upload assets | ||||||
|  |         uses: akkuman/gitea-release-action@v1 | ||||||
|  |         with: | ||||||
|  |           body: ${{ steps.changes.outputs.changes }} | ||||||
|  |           files: |- | ||||||
|  |             alr-bin*.deb | ||||||
|  |             alr-bin*.rpm | ||||||
|  |             alr-bin*.pkg.tar.zst | ||||||
|   | |||||||
| @@ -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
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -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 | ||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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 | ||||||
							
								
								
									
										36
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										36
									
								
								Makefile
									
									
									
									
									
								
							| @@ -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,7 +26,11 @@ build: check-no-root $(BIN) | |||||||
|  |  | ||||||
| export CGO_ENABLED := 0 | export CGO_ENABLED := 0 | ||||||
| $(BIN): | $(BIN): | ||||||
|  | ifeq ($(GENERATE),1) | ||||||
| 	go generate ./... | 	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: | ||||||
| @@ -32,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 $< $@ | ||||||
| @@ -55,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) | ||||||
|  |  | ||||||
| @@ -78,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: | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								README.md
									
									
									
									
									
								
							| @@ -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 | ||||||
|  |  | ||||||
| ## Спасибы | ## Спасибы | ||||||
|   | |||||||
| @@ -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.3%</text> |         <text x="86" y="15" fill="#010101" fill-opacity=".3">18.9%</text> | ||||||
|         <text x="86" y="14">19.3%</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 | 
							
								
								
									
										35
									
								
								build.go
									
									
									
									
									
								
							
							
						
						
									
										35
									
								
								build.go
									
									
									
									
									
								
							| @@ -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 { | ||||||
|   | |||||||
| @@ -76,6 +76,7 @@ var configKeys = []string{ | |||||||
| 	"autoPull", | 	"autoPull", | ||||||
| 	"logLevel", | 	"logLevel", | ||||||
| 	"ignorePkgUpdates", | 	"ignorePkgUpdates", | ||||||
|  | 	"updateSystemOnUpgrade", | ||||||
| } | } | ||||||
|  |  | ||||||
| func SetConfig() *cli.Command { | func SetConfig() *cli.Command { | ||||||
| @@ -137,6 +138,12 @@ func SetConfig() *cli.Command { | |||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 				deps.Cfg.System.SetIgnorePkgUpdates(updates) | 				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": | 			case "repo", "repos": | ||||||
| 				return cliutils.FormatCliExit(gotext.Get("use 'repo add/remove' commands to manage repositories"), nil) | 				return cliutils.FormatCliExit(gotext.Get("use 'repo add/remove' commands to manage repositories"), nil) | ||||||
| 			default: | 			default: | ||||||
| @@ -206,6 +213,8 @@ func GetConfig() *cli.Command { | |||||||
| 				} else { | 				} else { | ||||||
| 					fmt.Println(strings.Join(updates, ", ")) | 					fmt.Println(strings.Join(updates, ", ")) | ||||||
| 				} | 				} | ||||||
|  | 			case "updateSystemOnUpgrade": | ||||||
|  | 				fmt.Println(deps.Cfg.UpdateSystemOnUpgrade()) | ||||||
| 			case "repo", "repos": | 			case "repo", "repos": | ||||||
| 				repos := deps.Cfg.Repos() | 				repos := deps.Cfg.Repos() | ||||||
| 				if len(repos) == 0 { | 				if len(repos) == 0 { | ||||||
|   | |||||||
| @@ -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 | ||||||
| @@ -0,0 +1 @@ | |||||||
|  | alr-repo/foo-pkg 1.0.0-1 | ||||||
| @@ -0,0 +1,2 @@ | |||||||
|  | alr-repo/bar-pkg 1.0.0-1 | ||||||
|  | alr-repo/foo-pkg 1.0.0-1 | ||||||
| @@ -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(), "repo = []") |  | ||||||
| 		}, | 		}, | ||||||
| 	) | 	) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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") | ||||||
| 		}, | 		}, | ||||||
| 	) | 	) | ||||||
|   | |||||||
| @@ -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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -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)) | ||||||
|   | |||||||
| @@ -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$`}, |  | ||||||
| 			}) |  | ||||||
| 		}, |  | ||||||
| 	) |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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$\"") | ||||||
|   | |||||||
							
								
								
									
										40
									
								
								e2e-tests/issue_129_repo_toml_import_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								e2e-tests/issue_129_repo_toml_import_test.go
									
									
									
									
									
										Normal 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) | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
							
								
								
									
										63
									
								
								e2e-tests/issue_130_install_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								e2e-tests/issue_130_install_test.go
									
									
									
									
									
										Normal 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) | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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$\"") | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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") | ||||||
| 		}, | 		}, | ||||||
|   | |||||||
| @@ -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$") | ||||||
|   | |||||||
							
								
								
									
										50
									
								
								e2e-tests/issue_62_list_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								e2e-tests/issue_62_list_test.go
									
									
									
									
									
										Normal 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) | ||||||
|  | 		}, | ||||||
|  | 	) | ||||||
|  | } | ||||||
| @@ -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") | ||||||
| 		}, | 		}, | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -21,33 +21,29 @@ package e2etests_test | |||||||
| import ( | import ( | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/efficientgo/e2e" | 	"go.alt-gnome.ru/capytest" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestE2EIssue78Mirrors(t *testing.T) { | func TestE2EIssue78Mirrors(t *testing.T) { | ||||||
| 	dockerMultipleRun( | 	runMatrixSuite(t, "issue-78-mirrors", COMMON_SYSTEMS, func(t *testing.T, r capytest.Runner) { | ||||||
| 		t, | 		defaultPrepare(t, r) | ||||||
| 		"issue-78-mirrors", | 		execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git") | ||||||
| 		COMMON_SYSTEMS, | 		execShouldNoError(t, r, "sudo", "alr", "repo", "set-url", REPO_NAME_FOR_E2E_TESTS, "https://example.com") | ||||||
| 		func(t *testing.T, r e2e.Runnable) { | 		execShouldNoError(t, r, "sudo", "alr", "ref") | ||||||
| 			defaultPrepare(t, r) | 		execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "clear", REPO_NAME_FOR_E2E_TESTS) | ||||||
| 			execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", 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", "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", "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") | 		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") | 		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", "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") | 		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") | 		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", "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") | 		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") | 		execShouldError(t, r, "sudo", "alr", "ref") | ||||||
| 		}, | 	}, | ||||||
| 	) | 	) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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") | ||||||
|   | |||||||
| @@ -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")) | ||||||
| 		}, | 		}, | ||||||
| 	) | 	) | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,15 +21,15 @@ package e2etests_test | |||||||
| import ( | import ( | ||||||
| 	"testing" | 	"testing" | ||||||
|  |  | ||||||
| 	"github.com/efficientgo/e2e" | 	"go.alt-gnome.ru/capytest" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func TestE2EIssue95ConfigCommand(t *testing.T) { | func TestE2EIssue95ConfigCommand(t *testing.T) { | ||||||
| 	dockerMultipleRun( | 	runMatrixSuite( | ||||||
| 		t, | 		t, | ||||||
| 		"issue-95-config-command", | 		"issue-95-config-command", | ||||||
| 		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, "sh", "-c", "alr config show | grep \"autoPull: true\"") | 			execShouldNoError(t, r, "sh", "-c", "alr config show | grep \"autoPull: true\"") | ||||||
| 			execShouldNoError(t, r, "sh", "-c", "alr config get | grep \"autoPull: true\"") | 			execShouldNoError(t, r, "sh", "-c", "alr config get | grep \"autoPull: true\"") | ||||||
|   | |||||||
| @@ -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
									
									
									
									
									
								
							
							
						
						
									
										146
									
								
								fix.go
									
									
									
									
									
								
							| @@ -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
									
									
									
									
									
								
							
							
						
						
									
										23
									
								
								gen.go
									
									
									
									
									
								
							| @@ -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"), | ||||||
|  | 					}) | ||||||
|  | 				}, | ||||||
|  | 			}, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										416
									
								
								generators/plugin-generator/main.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										416
									
								
								generators/plugin-generator/main.go
									
									
									
									
									
										Normal 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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
							
								
								
									
										39
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										39
									
								
								go.mod
									
									
									
									
									
								
							| @@ -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,12 +8,10 @@ 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/goccy/go-yaml v1.18.0 | ||||||
| @@ -24,6 +20,11 @@ require ( | |||||||
| 	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 | ||||||
| @@ -31,9 +32,10 @@ require ( | |||||||
| 	github.com/muesli/reflow v0.3.0 | 	github.com/muesli/reflow v0.3.0 | ||||||
| 	github.com/pelletier/go-toml/v2 v2.2.4 | 	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 | ||||||
| @@ -71,21 +73,21 @@ 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/fatih/structs v1.1.0 // indirect |  | ||||||
| 	github.com/fsnotify/fsnotify v1.9.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.2.1 // 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.6.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 | ||||||
| @@ -102,15 +104,10 @@ require ( | |||||||
| 	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/knadh/koanf/maps v0.1.2 // indirect | ||||||
| 	github.com/knadh/koanf/parsers/json v1.0.0 // indirect | 	github.com/kr/pretty v0.3.1 // indirect | ||||||
| 	github.com/knadh/koanf/parsers/toml/v2 v2.2.0 // indirect | 	github.com/kr/text v0.2.0 // indirect | ||||||
| 	github.com/knadh/koanf/providers/confmap v1.0.0 // indirect |  | ||||||
| 	github.com/knadh/koanf/providers/env v1.1.0 // indirect |  | ||||||
| 	github.com/knadh/koanf/providers/file v1.2.0 // indirect |  | ||||||
| 	github.com/knadh/koanf/providers/rawbytes v1.0.0 // indirect |  | ||||||
| 	github.com/knadh/koanf/providers/structs v1.0.0 // indirect |  | ||||||
| 	github.com/knadh/koanf/v2 v2.2.1 // 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 | ||||||
| @@ -129,6 +126,7 @@ 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 | ||||||
| @@ -136,6 +134,10 @@ require ( | |||||||
| 	github.com/spf13/cast v1.7.1 // 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 | ||||||
| @@ -144,7 +146,6 @@ require ( | |||||||
| 	go4.org v0.0.0-20200411211856-f5505b9728dd // indirect | 	go4.org v0.0.0-20200411211856-f5505b9728dd // indirect | ||||||
| 	golang.org/x/mod v0.19.0 // indirect | 	golang.org/x/mod v0.19.0 // indirect | ||||||
| 	golang.org/x/net v0.38.0 // indirect | 	golang.org/x/net v0.38.0 // indirect | ||||||
| 	golang.org/x/oauth2 v0.25.0 // indirect |  | ||||||
| 	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 | ||||||
|   | |||||||
							
								
								
									
										74
									
								
								go.sum
									
									
									
									
									
								
							
							
						
						
									
										74
									
								
								go.sum
									
									
									
									
									
								
							| @@ -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.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= |  | ||||||
| github.com/cespare/xxhash/v2 v2.3.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= | ||||||
| @@ -110,6 +104,7 @@ github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEyc | |||||||
| 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= | ||||||
| @@ -139,13 +130,17 @@ github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6 | |||||||
| github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= | github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= | ||||||
| github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= | github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= | ||||||
| github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= | github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= | ||||||
| github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= |  | ||||||
| github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= |  | ||||||
| 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 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= | ||||||
| github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= | 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= | ||||||
| @@ -164,8 +159,8 @@ 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.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= | github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk= | ||||||
| github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= | 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= | ||||||
| @@ -204,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= | ||||||
| @@ -260,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= | ||||||
| @@ -281,8 +272,6 @@ 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 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= | ||||||
| github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= | ||||||
| github.com/knadh/koanf/parsers/json v1.0.0 h1:1pVR1JhMwbqSg5ICzU+surJmeBbdT4bQm7jjgnA+f8o= |  | ||||||
| github.com/knadh/koanf/parsers/json v1.0.0/go.mod h1:zb5WtibRdpxSoSJfXysqGbVxvbszdlroWDHGdDkkEYU= |  | ||||||
| github.com/knadh/koanf/parsers/toml/v2 v2.2.0 h1:2nV7tHYJ5OZy2BynQ4mOJ6k5bDqbbCzRERLUKBytz3A= | 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/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 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE= | ||||||
| @@ -291,10 +280,6 @@ github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWy | |||||||
| github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY= | 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 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/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA= | ||||||
| github.com/knadh/koanf/providers/rawbytes v1.0.0 h1:MrKDh/HksJlKJmaZjgs4r8aVBb/zsJyc/8qaSnzcdNI= |  | ||||||
| github.com/knadh/koanf/providers/rawbytes v1.0.0/go.mod h1:KxwYJf1uezTKy6PBtfE+m725NGp4GPVA7XoNTJ/PtLo= |  | ||||||
| github.com/knadh/koanf/providers/structs v1.0.0 h1:DznjB7NQykhqCar2LvNug3MuxEQsZ5KvfgMbio+23u4= |  | ||||||
| github.com/knadh/koanf/providers/structs v1.0.0/go.mod h1:kjo5TFtgpaZORlpoJqcbeLowM2cINodv8kX+oFAeQ1w= |  | ||||||
| github.com/knadh/koanf/v2 v2.2.1 h1:jaleChtw85y3UdBnI0wCqcg1sj1gPoz6D3caGNHtrNE= | 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/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= | ||||||
| @@ -308,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= | ||||||
| @@ -326,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= | ||||||
| @@ -353,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= | ||||||
| @@ -365,27 +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.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= |  | ||||||
| github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= |  | ||||||
| github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= | github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= | ||||||
| github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= | 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= | ||||||
| @@ -394,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= | ||||||
| @@ -427,10 +402,18 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf | |||||||
| 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= | ||||||
| @@ -449,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= | ||||||
| @@ -522,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.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= |  | ||||||
| golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= |  | ||||||
| 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= | ||||||
| @@ -561,8 +548,6 @@ 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.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= |  | ||||||
| golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= | ||||||
| golang.org/x/sys v0.33.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= | ||||||
| @@ -668,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= | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								info.go
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								info.go
									
									
									
									
									
								
							| @@ -31,7 +31,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/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 { | ||||||
|   | |||||||
| @@ -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. | ||||||
|   | |||||||
							
								
								
									
										196
									
								
								internal.go
									
									
									
									
									
								
							
							
						
						
									
										196
									
								
								internal.go
									
									
									
									
									
								
							| @@ -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 |  | ||||||
| 		}, |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|   | |||||||
| @@ -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,8 +339,12 @@ 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") | ||||||
| @@ -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 | ||||||
|   | |||||||
| @@ -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) { | ||||||
|   | |||||||
| @@ -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
									
								
							
							
						
						
									
										142
									
								
								internal/build/plugins.go
									
									
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
							
								
								
									
										60
									
								
								internal/build/plugins_executors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								internal/build/plugins_executors.go
									
									
									
									
									
										Normal 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) | ||||||
|  | } | ||||||
							
								
								
									
										369
									
								
								internal/build/plugins_executors_gen.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										369
									
								
								internal/build/plugins_executors_gen.go
									
									
									
									
									
										Normal 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 | ||||||
|  | } | ||||||
| @@ -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) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
| @@ -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 |  | ||||||
| } |  | ||||||
| @@ -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 |  | ||||||
| } |  | ||||||
| @@ -23,6 +23,7 @@ import ( | |||||||
| 	"os" | 	"os" | ||||||
| 	"strings" | 	"strings" | ||||||
|  |  | ||||||
|  | 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants" | ||||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl" | 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl" | ||||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache" | 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache" | ||||||
| ) | ) | ||||||
| @@ -74,7 +75,9 @@ func (s *SourceDownloader) DownloadSources( | |||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		opts.DlCache = dlcache.New(s.cfg.GetPaths().CacheDir) | 		// Используем временную директорию для загрузок | ||||||
|  | 		// 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 { | ||||||
|   | |||||||
| @@ -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 создает секцию содержимого пакета, которая содержит файлы, | ||||||
| @@ -167,8 +176,13 @@ func getBasePkgInfo(vars *alrsh.Package, input interface { | |||||||
| 	OsInfoProvider | 	OsInfoProvider | ||||||
| }, | }, | ||||||
| ) *nfpm.Info { | ) *nfpm.Info { | ||||||
|  | 	repo := input.Repository() | ||||||
|  | 	// Избегаем дублирования "alr-" префикса | ||||||
|  | 	if strings.HasPrefix(repo, "alr-") { | ||||||
|  | 		repo = repo[4:] // убираем "alr-" префикс | ||||||
|  | 	} | ||||||
| 	return &nfpm.Info{ | 	return &nfpm.Info{ | ||||||
| 		Name:    fmt.Sprintf("%s+alr-%s", vars.Name, input.Repository()), | 		Name:    fmt.Sprintf("%s+alr-%s", vars.Name, repo), | ||||||
| 		Arch:    cpu.Arch(), | 		Arch:    cpu.Arch(), | ||||||
| 		Version: vars.Version, | 		Version: vars.Version, | ||||||
| 		Release: overrides.ReleasePlatformSpecific(vars.Release, input.OSRelease()), | 		Release: overrides.ReleasePlatformSpecific(vars.Release, input.OSRelease()), | ||||||
|   | |||||||
| @@ -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 { | ||||||
|   | |||||||
| @@ -21,11 +21,14 @@ package config | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"os" | ||||||
|  | 	"os/exec" | ||||||
| 	"path/filepath" | 	"path/filepath" | ||||||
|  |  | ||||||
| 	"github.com/goccy/go-yaml" | 	"github.com/goccy/go-yaml" | ||||||
| 	"github.com/knadh/koanf/providers/confmap" | 	"github.com/knadh/koanf/providers/confmap" | ||||||
| 	"github.com/knadh/koanf/v2" | 	"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" | ||||||
| @@ -55,7 +58,13 @@ func defaultConfigKoanf() *koanf.Koanf { | |||||||
| 		"ignorePkgUpdates": []string{}, | 		"ignorePkgUpdates": []string{}, | ||||||
| 		"logLevel":         "info", | 		"logLevel":         "info", | ||||||
| 		"autoPull":         true, | 		"autoPull":         true, | ||||||
| 		"repos":            []types.Repo{}, | 		"updateSystemOnUpgrade": false, | ||||||
|  | 		"repos": []types.Repo{ | ||||||
|  | 			{ | ||||||
|  | 				Name: "alr-default", | ||||||
|  | 				URL:  "https://gitea.plemya-x.ru/Plemya-x/alr-default.git", | ||||||
|  | 			}, | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| 	if err := k.Load(confmap.Provider(defaults, "."), nil); err != nil { | 	if err := k.Load(confmap.Provider(defaults, "."), nil); err != nil { | ||||||
| 		panic(k) | 		panic(k) | ||||||
| @@ -98,8 +107,20 @@ func (c *ALRConfig) Load() error { | |||||||
| 	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") | ||||||
|  |  | ||||||
|  | 	// Проверяем существование кэш-директории, но не пытаемся создать | ||||||
|  | 	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 | ||||||
| } | } | ||||||
| @@ -112,6 +133,126 @@ func (c *ALRConfig) ToYAML() (string, error) { | |||||||
| 	return string(data), nil | 	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 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) RootCmd() string             { return c.cfg.RootCmd } | ||||||
| func (c *ALRConfig) PagerStyle() string          { return c.cfg.PagerStyle } | func (c *ALRConfig) PagerStyle() string          { return c.cfg.PagerStyle } | ||||||
| func (c *ALRConfig) AutoPull() bool              { return c.cfg.AutoPull } | func (c *ALRConfig) AutoPull() bool              { return c.cfg.AutoPull } | ||||||
| @@ -120,4 +261,5 @@ func (c *ALRConfig) SetRepos(repos []types.Repo) { c.System.SetRepos(repos) } | |||||||
| func (c *ALRConfig) IgnorePkgUpdates() []string  { return c.cfg.IgnorePkgUpdates } | func (c *ALRConfig) IgnorePkgUpdates() []string  { return c.cfg.IgnorePkgUpdates } | ||||||
| func (c *ALRConfig) LogLevel() string            { return c.cfg.LogLevel } | func (c *ALRConfig) LogLevel() string            { return c.cfg.LogLevel } | ||||||
| func (c *ALRConfig) UseRootCmd() bool            { return c.cfg.UseRootCmd } | 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 } | func (c *ALRConfig) GetPaths() *Paths            { return c.paths } | ||||||
|   | |||||||
| @@ -142,3 +142,10 @@ func (c *SystemConfig) SetRepos(v []types.Repo) { | |||||||
| 		panic(err) | 		panic(err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func (c *SystemConfig) SetUpdateSystemOnUpgrade(v bool) { | ||||||
|  | 	err := c.k.Set("updateSystemOnUpgrade", v) | ||||||
|  | 	if err != nil { | ||||||
|  | 		panic(err) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ 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 - устарело, используйте GetPrivilegedGroup() | ||||||
|  | 	PrivilegedGroup  = "wheel" // оставлено для обратной совместимости | ||||||
| ) | ) | ||||||
|   | |||||||
| @@ -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
									
								
							
							
						
						
									
										663
									
								
								internal/gen/aur.go
									
									
									
									
									
										Normal 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) | ||||||
|  | } | ||||||
							
								
								
									
										133
									
								
								internal/gen/tmpls/aur.tmpl.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										133
									
								
								internal/gen/tmpls/aur.tmpl.sh
									
									
									
									
									
										Normal 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}} | ||||||
|  | } | ||||||
| @@ -140,16 +140,19 @@ func (a *APT) ListInstalled(opts *Opts) (map[string]string, error) { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (a *APT) IsInstalled(pkg string) (bool, error) { | func (a *APT) IsInstalled(pkg string) (bool, error) { | ||||||
| 	cmd := exec.Command("dpkg-query", "-l", pkg) | 	cmd := exec.Command("dpkg-query", "-f", "${Status}", "-W", pkg) | ||||||
| 	output, err := cmd.CombinedOutput() | 	output, err := cmd.CombinedOutput() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		if exitErr, ok := err.(*exec.ExitError); ok { | 		if exitErr, ok := err.(*exec.ExitError); ok { | ||||||
| 			// Exit code 1 means the package is not installed | 			// Код выхода 1 означает что пакет не найден | ||||||
| 			if exitErr.ExitCode() == 1 { | 			if exitErr.ExitCode() == 1 { | ||||||
| 				return false, nil | 				return false, nil | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 		return false, fmt.Errorf("apt: isinstalled: %w, output: %s", err, output) | 		return false, fmt.Errorf("apt: isinstalled: %w, output: %s", err, output) | ||||||
| 	} | 	} | ||||||
| 	return true, nil |  | ||||||
|  | 	status := strings.TrimSpace(string(output)) | ||||||
|  | 	// Проверяем что пакет действительно установлен (статус должен содержать "install ok installed") | ||||||
|  | 	return strings.Contains(status, "install ok installed"), nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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...) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -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) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -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) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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 | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -68,7 +68,7 @@ func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	for _, repo := range repos { | 	for _, repo := range repos { | ||||||
| 		err := rs.pullRepo(ctx, repo) | 		err := rs.pullRepo(ctx, &repo, false) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| @@ -77,7 +77,16 @@ func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error { | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (rs *Repos) pullRepo(ctx context.Context, repo types.Repo) error { | func (rs *Repos) PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) error { | ||||||
|  | 	err := rs.pullRepo(ctx, repo, true) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func (rs *Repos) pullRepo(ctx context.Context, repo *types.Repo, updateRepoFromToml bool) error { | ||||||
| 	urls := []string{repo.URL} | 	urls := []string{repo.URL} | ||||||
| 	urls = append(urls, repo.Mirrors...) | 	urls = append(urls, repo.Mirrors...) | ||||||
|  |  | ||||||
| @@ -88,7 +97,7 @@ func (rs *Repos) pullRepo(ctx context.Context, repo types.Repo) error { | |||||||
| 			slog.Info(gotext.Get("Trying mirror"), "repo", repo.Name, "mirror", repoURL) | 			slog.Info(gotext.Get("Trying mirror"), "repo", repo.Name, "mirror", repoURL) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		err := rs.pullRepoFromURL(ctx, repoURL, repo) | 		err := rs.pullRepoFromURL(ctx, repoURL, repo, updateRepoFromToml) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			lastErr = err | 			lastErr = err | ||||||
| 			slog.Warn(gotext.Get("Failed to pull from URL"), "repo", repo.Name, "url", repoURL, "error", err) | 			slog.Warn(gotext.Get("Failed to pull from URL"), "repo", repo.Name, "url", repoURL, "error", err) | ||||||
| @@ -149,7 +158,7 @@ func readGitRepo(repoDir, repoUrl string) (*git.Repository, bool, error) { | |||||||
| 	return r, true, nil | 	return r, true, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func (rs *Repos) pullRepoFromURL(ctx context.Context, rawRepoUrl string, repo types.Repo) error { | func (rs *Repos) pullRepoFromURL(ctx context.Context, rawRepoUrl string, repo *types.Repo, update bool) error { | ||||||
| 	repoURL, err := url.Parse(rawRepoUrl) | 	repoURL, err := url.Parse(rawRepoUrl) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("invalid URL %s: %w", rawRepoUrl, err) | 		return fmt.Errorf("invalid URL %s: %w", rawRepoUrl, err) | ||||||
| @@ -214,12 +223,12 @@ func (rs *Repos) pullRepoFromURL(ctx context.Context, rawRepoUrl string, repo ty | |||||||
| 	// empty. In this case, we need to update the DB fully | 	// empty. In this case, we need to update the DB fully | ||||||
| 	// rather than just incrementally. | 	// rather than just incrementally. | ||||||
| 	if rs.db.IsEmpty() || freshGit { | 	if rs.db.IsEmpty() || freshGit { | ||||||
| 		err = rs.processRepoFull(ctx, repo, repoDir) | 		err = rs.processRepoFull(ctx, *repo, repoDir) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 	} else { | 	} else { | ||||||
| 		err = rs.processRepoChanges(ctx, repo, r, w, old, new) | 		err = rs.processRepoChanges(ctx, *repo, r, w, old, new) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| @@ -247,6 +256,18 @@ func (rs *Repos) pullRepoFromURL(ctx context.Context, rawRepoUrl string, repo ty | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	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 | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										106
									
								
								internal/stats/tracker.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								internal/stats/tracker.go
									
									
									
									
									
										Normal 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") | ||||||
|  | } | ||||||
| @@ -9,52 +9,52 @@ 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 "" | ||||||
|  |  | ||||||
| @@ -218,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 "" | ||||||
|  |  | ||||||
| @@ -403,27 +407,27 @@ msgstr "" | |||||||
| msgid "ERROR" | msgid "ERROR" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:88 | #: internal/repos/pull.go:97 | ||||||
| msgid "Trying mirror" | msgid "Trying mirror" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:94 | #: internal/repos/pull.go:103 | ||||||
| msgid "Failed to pull from URL" | msgid "Failed to pull from URL" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:158 | #: internal/repos/pull.go:167 | ||||||
| msgid "Pulling repository" | msgid "Pulling repository" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:195 | #: internal/repos/pull.go:204 | ||||||
| msgid "Repository up to date" | msgid "Repository up to date" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:230 | #: 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:246 | #: 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." | ||||||
| @@ -441,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 "" | ||||||
| @@ -477,11 +485,11 @@ msgstr "" | |||||||
| msgid "Enable interactive questions and prompts" | msgid "Enable interactive questions and prompts" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: main.go:147 | #: main.go:148 | ||||||
| msgid "Show help" | msgid "Show help" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: main.go:151 | #: main.go:152 | ||||||
| msgid "Error while running app" | msgid "Error while running app" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| @@ -513,44 +521,44 @@ msgstr "" | |||||||
| msgid "Pull all repositories that have changed" | msgid "Pull all repositories that have changed" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:41 | #: repo.go:42 | ||||||
| msgid "Manage repos" | msgid "Manage repos" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:55 repo.go:625 | #: repo.go:56 repo.go:625 | ||||||
| msgid "Remove an existing repository" | msgid "Remove an existing repository" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:57 repo.go:521 | #: repo.go:58 repo.go:521 | ||||||
| msgid "<name>" | msgid "<name>" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:102 repo.go:465 repo.go:568 | #: 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:109 | #: repo.go:110 | ||||||
| msgid "Error removing repo directory" | msgid "Error removing repo directory" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:113 repo.go:180 repo.go:253 repo.go:316 repo.go:389 repo.go:504 | #: repo.go:114 repo.go:195 repo.go:253 repo.go:316 repo.go:389 repo.go:504 | ||||||
| #: repo.go:576 | #: repo.go:576 | ||||||
| msgid "Error saving config" | msgid "Error saving config" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:132 | #: repo.go:133 | ||||||
| msgid "Error removing packages from database" | msgid "Error removing packages from database" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:143 repo.go:595 | #: repo.go:144 repo.go:595 | ||||||
| msgid "Add a new repository" | msgid "Add a new repository" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:144 repo.go:270 repo.go:345 repo.go:402 | #: repo.go:145 repo.go:270 repo.go:345 repo.go:402 | ||||||
| msgid "<name> <url>" | msgid "<name> <url>" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
| #: repo.go:169 | #: repo.go:170 | ||||||
| msgid "Repo \"%s\" already exists" | msgid "Repo \"%s\" already exists" | ||||||
| msgstr "" | msgstr "" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,63 +5,63 @@ | |||||||
| msgid "" | msgid "" | ||||||
| msgstr "" | msgstr "" | ||||||
| "Project-Id-Version: unnamed project\n" | "Project-Id-Version: unnamed project\n" | ||||||
| "PO-Revision-Date: 2025-06-29 21: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 "Сделано" | ||||||
|  |  | ||||||
| @@ -225,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 "Установка зависимостей" | ||||||
|  |  | ||||||
| @@ -400,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" | ||||||
| @@ -417,27 +421,27 @@ msgstr "" | |||||||
| msgid "ERROR" | msgid "ERROR" | ||||||
| msgstr "ОШИБКА" | msgstr "ОШИБКА" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:88 | #: internal/repos/pull.go:97 | ||||||
| msgid "Trying mirror" | msgid "Trying mirror" | ||||||
| msgstr "Пробую зеркало" | msgstr "Пробую зеркало" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:94 | #: internal/repos/pull.go:103 | ||||||
| msgid "Failed to pull from URL" | msgid "Failed to pull from URL" | ||||||
| msgstr "Не удалось извлечь из URL" | msgstr "Не удалось извлечь из URL" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:158 | #: internal/repos/pull.go:167 | ||||||
| msgid "Pulling repository" | msgid "Pulling repository" | ||||||
| msgstr "Скачивание репозитория" | msgstr "Скачивание репозитория" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:195 | #: internal/repos/pull.go:204 | ||||||
| msgid "Repository up to date" | msgid "Repository up to date" | ||||||
| msgstr "Репозиторий уже обновлён" | msgstr "Репозиторий уже обновлён" | ||||||
|  |  | ||||||
| #: internal/repos/pull.go:230 | #: 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:246 | #: 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." | ||||||
| @@ -457,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 и выйти" | ||||||
| @@ -493,11 +501,11 @@ msgstr "Аргументы, которые будут переданы мене | |||||||
| msgid "Enable interactive questions and prompts" | msgid "Enable interactive questions and prompts" | ||||||
| msgstr "Включение интерактивных вопросов и запросов" | msgstr "Включение интерактивных вопросов и запросов" | ||||||
|  |  | ||||||
| #: main.go:147 | #: main.go:148 | ||||||
| msgid "Show help" | msgid "Show help" | ||||||
| msgstr "Показать справку" | msgstr "Показать справку" | ||||||
|  |  | ||||||
| #: main.go:151 | #: main.go:152 | ||||||
| msgid "Error while running app" | msgid "Error while running app" | ||||||
| msgstr "Ошибка при запуске приложения" | msgstr "Ошибка при запуске приложения" | ||||||
|  |  | ||||||
| @@ -529,44 +537,44 @@ msgstr "%s %s загружается — %s/с\n" | |||||||
| msgid "Pull all repositories that have changed" | msgid "Pull all repositories that have changed" | ||||||
| msgstr "Скачать все изменённые репозитории" | msgstr "Скачать все изменённые репозитории" | ||||||
|  |  | ||||||
| #: repo.go:41 | #: repo.go:42 | ||||||
| msgid "Manage repos" | msgid "Manage repos" | ||||||
| msgstr "Управление репозиториями" | msgstr "Управление репозиториями" | ||||||
|  |  | ||||||
| #: repo.go:55 repo.go:625 | #: repo.go:56 repo.go:625 | ||||||
| msgid "Remove an existing repository" | msgid "Remove an existing repository" | ||||||
| msgstr "Удалить существующий репозиторий" | msgstr "Удалить существующий репозиторий" | ||||||
|  |  | ||||||
| #: repo.go:57 repo.go:521 | #: repo.go:58 repo.go:521 | ||||||
| msgid "<name>" | msgid "<name>" | ||||||
| msgstr "<имя>" | msgstr "<имя>" | ||||||
|  |  | ||||||
| #: repo.go:102 repo.go:465 repo.go:568 | #: 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:109 | #: repo.go:110 | ||||||
| msgid "Error removing repo directory" | msgid "Error removing repo directory" | ||||||
| msgstr "Ошибка при удалении каталога репозитория" | msgstr "Ошибка при удалении каталога репозитория" | ||||||
|  |  | ||||||
| #: repo.go:113 repo.go:180 repo.go:253 repo.go:316 repo.go:389 repo.go:504 | #: repo.go:114 repo.go:195 repo.go:253 repo.go:316 repo.go:389 repo.go:504 | ||||||
| #: repo.go:576 | #: repo.go:576 | ||||||
| msgid "Error saving config" | msgid "Error saving config" | ||||||
| msgstr "Ошибка при сохранении конфигурации" | msgstr "Ошибка при сохранении конфигурации" | ||||||
|  |  | ||||||
| #: repo.go:132 | #: repo.go:133 | ||||||
| msgid "Error removing packages from database" | msgid "Error removing packages from database" | ||||||
| msgstr "Ошибка при удалении пакетов из базы данных" | msgstr "Ошибка при удалении пакетов из базы данных" | ||||||
|  |  | ||||||
| #: repo.go:143 repo.go:595 | #: repo.go:144 repo.go:595 | ||||||
| msgid "Add a new repository" | msgid "Add a new repository" | ||||||
| msgstr "Добавить новый репозиторий" | msgstr "Добавить новый репозиторий" | ||||||
|  |  | ||||||
| #: repo.go:144 repo.go:270 repo.go:345 repo.go:402 | #: repo.go:145 repo.go:270 repo.go:345 repo.go:402 | ||||||
| msgid "<name> <url>" | msgid "<name> <url>" | ||||||
| msgstr "<имя> <url>" | msgstr "<имя> <url>" | ||||||
|  |  | ||||||
| #: repo.go:169 | #: repo.go:170 | ||||||
| msgid "Repo \"%s\" already exists" | msgid "Repo \"%s\" already exists" | ||||||
| msgstr "Репозиторий \"%s\" уже существует" | msgstr "Репозиторий \"%s\" уже существует" | ||||||
|  |  | ||||||
| @@ -707,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 "Ошибка при установке нативных пакетов" | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,136 +17,41 @@ | |||||||
| 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" | ||||||
|  |  | ||||||
| 	"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/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 | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	group, err := user.LookupGroup(constants.PrivilegedGroup) | 	privilegedGroup := GetPrivilegedGroup() | ||||||
|  | 	group, err := user.LookupGroup(privilegedGroup) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| @@ -161,27 +66,7 @@ func EnuseIsPrivilegedGroupMember() error { | |||||||
| 			return nil | 			return nil | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	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", 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 { | ||||||
|   | |||||||
							
								
								
									
										76
									
								
								internal/utils/privileged_group.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								internal/utils/privileged_group.go
									
									
									
									
									
										Normal 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 utils | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"os/user" | ||||||
|  | 	"sync" | ||||||
|  |  | ||||||
|  | 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	privilegedGroupCache string | ||||||
|  | 	privilegedGroupOnce  sync.Once | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // GetPrivilegedGroup определяет правильную привилегированную группу для текущего дистрибутива. | ||||||
|  | // Дистрибутивы на базе Debian/Ubuntu используют группу "sudo", остальные - "wheel". | ||||||
|  | func GetPrivilegedGroup() string { | ||||||
|  | 	privilegedGroupOnce.Do(func() { | ||||||
|  | 		privilegedGroupCache = detectPrivilegedGroup() | ||||||
|  | 	}) | ||||||
|  | 	return privilegedGroupCache | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func detectPrivilegedGroup() string { | ||||||
|  | 	// Попробуем определить дистрибутив | ||||||
|  | 	ctx := context.Background() | ||||||
|  | 	osInfo, err := distro.ParseOSRelease(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		// Если не можем определить дистрибутив, проверяем какие группы существуют | ||||||
|  | 		return detectGroupByAvailability() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Проверяем ID и семейство дистрибутива | ||||||
|  | 	// Debian и его производные (Ubuntu, Mint, PopOS и т.д.) используют sudo | ||||||
|  | 	if osInfo.ID == "debian" || osInfo.ID == "ubuntu" { | ||||||
|  | 		return "sudo" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Проверяем семейство дистрибутива через ID_LIKE | ||||||
|  | 	for _, like := range osInfo.Like { | ||||||
|  | 		if like == "debian" || like == "ubuntu" { | ||||||
|  | 			return "sudo" | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Для остальных дистрибутивов (Fedora, RHEL, Arch, openSUSE, ALT Linux) используется wheel | ||||||
|  | 	return "wheel" | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // detectGroupByAvailability проверяет существование групп в системе | ||||||
|  | func detectGroupByAvailability() string { | ||||||
|  | 	// Сначала проверяем группу sudo (более распространена) | ||||||
|  | 	if _, err := user.LookupGroup("sudo"); err == nil { | ||||||
|  | 		return "sudo" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Если sudo не найдена, возвращаем wheel | ||||||
|  | 	return "wheel" | ||||||
|  | } | ||||||
| @@ -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 с правами для привилегированной группы | ||||||
|  | // Все каталоги в /tmp/alr принадлежат root:привилегированная_группа с правами 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 | ||||||
|  | 		} | ||||||
|  | 		 | ||||||
|  | 		// Для обычной работы устанавливаем права и привилегированную группу | ||||||
|  | 		permissions := "2775" | ||||||
|  | 		group := GetPrivilegedGroup() | ||||||
|  | 		 | ||||||
|  | 		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 игнорируем ошибки, если группа не существует | ||||||
|  | 			if !isRoot { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		 | ||||||
|  | 		// Устанавливаем владельца root:wheel | ||||||
|  | 		err = chownCmd.Run() | ||||||
|  | 		if err != nil { | ||||||
|  | 			// Для root игнорируем ошибки, если группа не существует | ||||||
|  | 			if !isRoot { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		 | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | 	 | ||||||
|  | 	// Для остальных каталогов обычное создание | ||||||
|  | 	return os.MkdirAll(path, mode) | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										41
									
								
								list.go
									
									
									
									
									
								
							
							
						
						
									
										41
									
								
								list.go
									
									
									
									
									
								
							| @@ -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 { | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								main.go
									
									
									
									
									
								
							| @@ -87,7 +87,7 @@ func GetApp() *cli.App { | |||||||
| 			// 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 != "" { | ||||||
|   | |||||||
							
								
								
									
										42
									
								
								pkg/dl/dl.go
									
									
									
									
									
								
							
							
						
						
									
										42
									
								
								pkg/dl/dl.go
									
									
									
									
									
								
							| @@ -280,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 | ||||||
| 		} | 		} | ||||||
| @@ -296,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 | ||||||
| @@ -317,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) | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -22,6 +22,7 @@ package dl | |||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
|  | 	"fmt" | ||||||
| 	"net/url" | 	"net/url" | ||||||
| 	"path" | 	"path" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| @@ -149,6 +150,7 @@ func (d *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") | ||||||
| @@ -177,27 +179,67 @@ func (d *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 err != nil { | 	if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { | ||||||
| 		if errors.Is(err, git.NoErrAlreadyUpToDate) { |  | ||||||
| 			return false, nil |  | ||||||
| 		} |  | ||||||
| 		return false, err | 		return false, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// 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) | 	err = VerifyHashFromLocal("", opts) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return false, err | 		return false, err | ||||||
|   | |||||||
| @@ -153,7 +153,7 @@ func TestGitDownloaderUpdate(t *testing.T) { | |||||||
| 		assert.NoError(t, err) | 		assert.NoError(t, err) | ||||||
|  |  | ||||||
| 		updated, err := d.Update(dl.Options{ | 		updated, err := d.Update(dl.Options{ | ||||||
| 			URL:           "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git~rev=test-update-git-downloader", | 			URL:           "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git?~rev=test-update-git-downloader", | ||||||
| 			Destination:   dest, | 			Destination:   dest, | ||||||
| 			Hash:          hsh, | 			Hash:          hsh, | ||||||
| 			HashAlgorithm: "sha256", | 			HashAlgorithm: "sha256", | ||||||
|   | |||||||
| @@ -61,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 | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										37
									
								
								pkg/dlcache/dlcache_prod.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								pkg/dlcache/dlcache_prod.go
									
									
									
									
									
										Normal 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) | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -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,7 +57,7 @@ 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) { | ||||||
| @@ -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() { | ||||||
|   | |||||||
							
								
								
									
										28
									
								
								pkg/dlcache/dlcache_test_impl.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								pkg/dlcache/dlcache_test_impl.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,28 @@ | |||||||
|  | //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" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // createDir создает директорию с обычными правами для тестирования | ||||||
|  | func createDir(itemPath string, mode os.FileMode) error { | ||||||
|  | 	return os.MkdirAll(itemPath, mode) | ||||||
|  | } | ||||||
| @@ -21,13 +21,14 @@ package types | |||||||
|  |  | ||||||
| // Config represents the ALR configuration file | // Config represents the ALR configuration file | ||||||
| type Config struct { | type Config struct { | ||||||
| 	RootCmd          string   `json:"rootCmd" koanf:"rootCmd"` | 	RootCmd             string   `json:"rootCmd" koanf:"rootCmd"` | ||||||
| 	UseRootCmd       bool     `json:"useRootCmd" koanf:"useRootCmd"` | 	UseRootCmd          bool     `json:"useRootCmd" koanf:"useRootCmd"` | ||||||
| 	PagerStyle       string   `json:"pagerStyle" koanf:"pagerStyle"` | 	PagerStyle          string   `json:"pagerStyle" koanf:"pagerStyle"` | ||||||
| 	IgnorePkgUpdates []string `json:"ignorePkgUpdates" koanf:"ignorePkgUpdates"` | 	IgnorePkgUpdates    []string `json:"ignorePkgUpdates" koanf:"ignorePkgUpdates"` | ||||||
| 	Repos            []Repo   `json:"repo" koanf:"repo"` | 	Repos               []Repo   `json:"repo" koanf:"repo"` | ||||||
| 	AutoPull         bool     `json:"autoPull" koanf:"autoPull"` | 	AutoPull            bool     `json:"autoPull" koanf:"autoPull"` | ||||||
| 	LogLevel         string   `json:"logLevel" koanf:"logLevel"` | 	LogLevel            string   `json:"logLevel" koanf:"logLevel"` | ||||||
|  | 	UpdateSystemOnUpgrade bool   `json:"updateSystemOnUpgrade" koanf:"updateSystemOnUpgrade"` | ||||||
| } | } | ||||||
|  |  | ||||||
| // Repo represents a ALR repo within a configuration file | // Repo represents a ALR repo within a configuration file | ||||||
|   | |||||||
| @@ -22,6 +22,9 @@ package types | |||||||
| // RepoConfig represents a ALR repo's alr-repo.toml file. | // RepoConfig represents a ALR repo's alr-repo.toml file. | ||||||
| type RepoConfig struct { | type RepoConfig struct { | ||||||
| 	Repo struct { | 	Repo struct { | ||||||
| 		MinVersion string `toml:"minVersion"` | 		MinVersion string   `toml:"minVersion"` | ||||||
|  | 		URL        string   `toml:"url"` | ||||||
|  | 		Ref        string   `toml:"ref"` | ||||||
|  | 		Mirrors    []string `toml:"mirrors"` | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -21,7 +21,6 @@ import ( | |||||||
| 	"github.com/urfave/cli/v2" | 	"github.com/urfave/cli/v2" | ||||||
|  |  | ||||||
| 	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/utils" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func RefreshCmd() *cli.Command { | func RefreshCmd() *cli.Command { | ||||||
| @@ -30,9 +29,6 @@ func RefreshCmd() *cli.Command { | |||||||
| 		Usage:   gotext.Get("Pull all repositories that have changed"), | 		Usage:   gotext.Get("Pull all repositories that have changed"), | ||||||
| 		Aliases: []string{"ref"}, | 		Aliases: []string{"ref"}, | ||||||
| 		Action: func(c *cli.Context) error { | 		Action: func(c *cli.Context) error { | ||||||
| 			if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			ctx := c.Context | 			ctx := c.Context | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										37
									
								
								repo.go
									
									
									
									
									
								
							
							
						
						
									
										37
									
								
								repo.go
									
									
									
									
									
								
							| @@ -29,6 +29,7 @@ import ( | |||||||
| 	"github.com/urfave/cli/v2" | 	"github.com/urfave/cli/v2" | ||||||
| 	"golang.org/x/exp/slices" | 	"golang.org/x/exp/slices" | ||||||
|  |  | ||||||
|  | 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/build" | ||||||
| 	"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/utils" | 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils" | ||||||
| @@ -113,9 +114,6 @@ func RemoveRepoCmd() *cli.Command { | |||||||
| 				return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) | 				return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			deps, err = appbuilder. | 			deps, err = appbuilder. | ||||||
| 				New(ctx). | 				New(ctx). | ||||||
| @@ -169,10 +167,24 @@ func AddRepoCmd() *cli.Command { | |||||||
| 					return cliutils.FormatCliExit(gotext.Get("Repo \"%s\" already exists", repo.Name), nil) | 					return cliutils.FormatCliExit(gotext.Get("Repo \"%s\" already exists", repo.Name), nil) | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
| 			reposSlice = append(reposSlice, types.Repo{ |  | ||||||
|  | 			newRepo := types.Repo{ | ||||||
| 				Name: name, | 				Name: name, | ||||||
| 				URL:  repoURL, | 				URL:  repoURL, | ||||||
| 			}) | 			} | ||||||
|  |  | ||||||
|  | 			r, close, err := build.GetSafeReposExecutor() | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			defer close() | ||||||
|  |  | ||||||
|  | 			newRepo, err = r.PullOneAndUpdateFromConfig(c.Context, &newRepo) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			reposSlice = append(reposSlice, newRepo) | ||||||
| 			cfg.SetRepos(reposSlice) | 			cfg.SetRepos(reposSlice) | ||||||
|  |  | ||||||
| 			err = cfg.System.Save() | 			err = cfg.System.Save() | ||||||
| @@ -180,21 +192,6 @@ func AddRepoCmd() *cli.Command { | |||||||
| 				return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) | 				return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			deps, err = appbuilder. |  | ||||||
| 				New(ctx). |  | ||||||
| 				UseConfig(cfg). |  | ||||||
| 				WithDB(). |  | ||||||
| 				WithReposForcePull(). |  | ||||||
| 				Build() |  | ||||||
| 			if err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
| 			defer deps.Defer() |  | ||||||
|  |  | ||||||
| 			return nil | 			return nil | ||||||
| 		}), | 		}), | ||||||
| 	} | 	} | ||||||
|   | |||||||
							
								
								
									
										37
									
								
								scripts/fmt-precommit.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										37
									
								
								scripts/fmt-precommit.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | # 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/>. | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | # Запускаем форматирование | ||||||
|  | make fmt || true | ||||||
|  |  | ||||||
|  | # Проверяем какие файлы были изменены (только те, что отслеживаются git) | ||||||
|  | CHANGED_FILES=$(git diff --name-only --diff-filter=M | grep '\.go$' || true) | ||||||
|  |  | ||||||
|  | # Если файлы были изменены, добавляем их в git | ||||||
|  | if [ ! -z "$CHANGED_FILES" ]; then | ||||||
|  |     echo "Formatting changed the following files:" | ||||||
|  |     echo "$CHANGED_FILES" | ||||||
|  |     # Добавляем только измененные файлы, которые уже отслеживаются | ||||||
|  |     echo "$CHANGED_FILES" | xargs -r git add | ||||||
|  |     echo "Files were formatted and staged" | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | echo "Formatting completed" | ||||||
|  | # Всегда возвращаем успех | ||||||
|  | exit 0 | ||||||
							
								
								
									
										63
									
								
								scripts/i18n-precommit.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										63
									
								
								scripts/i18n-precommit.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | # 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/>. | ||||||
|  |  | ||||||
|  | # Wrapper script for i18n that automatically stages changed files for pre-commit | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | # Сохраняем состояние файлов до выполнения i18n | ||||||
|  | TRANSLATION_FILES=( | ||||||
|  |     "internal/translations/default.pot" | ||||||
|  |     "internal/translations/po/ru/default.po" | ||||||
|  |     "assets/i18n-ru-badge.svg" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | # Создаем временные файлы для сравнения | ||||||
|  | TEMP_DIR=$(mktemp -d) | ||||||
|  | for file in "${TRANSLATION_FILES[@]}"; do | ||||||
|  |     if [[ -f "$file" ]]; then | ||||||
|  |         cp "$file" "$TEMP_DIR/$(basename "$file")" | ||||||
|  |     fi | ||||||
|  | done | ||||||
|  |  | ||||||
|  | # Выполняем обновление переводов | ||||||
|  | make i18n | ||||||
|  |  | ||||||
|  | # Проверяем какие файлы изменились и добавляем их в staging area | ||||||
|  | CHANGED_FILES=() | ||||||
|  | for file in "${TRANSLATION_FILES[@]}"; do | ||||||
|  |     if [[ -f "$file" ]]; then | ||||||
|  |         if [[ ! -f "$TEMP_DIR/$(basename "$file")" ]] || ! cmp -s "$file" "$TEMP_DIR/$(basename "$file")"; then | ||||||
|  |             CHANGED_FILES+=("$file") | ||||||
|  |         fi | ||||||
|  |     fi | ||||||
|  | done | ||||||
|  |  | ||||||
|  | # Добавляем измененные файлы в git staging area | ||||||
|  | if [[ ${#CHANGED_FILES[@]} -gt 0 ]]; then | ||||||
|  |     echo "Auto-staging changed translation files:" | ||||||
|  |     for file in "${CHANGED_FILES[@]}"; do | ||||||
|  |         echo "  - $file" | ||||||
|  |         git add "$file" | ||||||
|  |     done | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | # Очищаем временные файлы | ||||||
|  | rm -rf "$TEMP_DIR" | ||||||
|  |  | ||||||
|  | # Выход с кодом 0 (успех) даже если файлы были изменены | ||||||
|  | exit 0 | ||||||
| @@ -32,12 +32,20 @@ error() { | |||||||
|  |  | ||||||
| installPkg() { | installPkg() { | ||||||
|   rootCmd="" |   rootCmd="" | ||||||
|   if command -v doas &>/dev/null; then |    | ||||||
|     rootCmd="doas" |   # Проверяем, запущен ли скрипт от root | ||||||
|   elif command -v sudo &>/dev/null; then |   if [ "$(id -u)" = "0" ]; then | ||||||
|     rootCmd="sudo" |     # Если root, не используем sudo/doas | ||||||
|  |     rootCmd="" | ||||||
|   else |   else | ||||||
|     warn "Не обнаружена команда повышения привилегий (например, sudo, doas)" |     # Если не root, ищем команду повышения привилегий | ||||||
|  |     if command -v doas &>/dev/null; then | ||||||
|  |       rootCmd="doas" | ||||||
|  |     elif command -v sudo &>/dev/null; then | ||||||
|  |       rootCmd="sudo" | ||||||
|  |     else | ||||||
|  |       warn "Не обнаружена команда повышения привилегий (например, sudo, doas)" | ||||||
|  |     fi | ||||||
|   fi |   fi | ||||||
|  |  | ||||||
|   case $1 in |   case $1 in | ||||||
| @@ -48,10 +56,46 @@ installPkg() { | |||||||
|   esac |   esac | ||||||
| } | } | ||||||
|  |  | ||||||
|  | trackInstallation() { | ||||||
|  |   # Отправить статистику установки (не критично если не получится) | ||||||
|  |   if command -v curl &>/dev/null; then | ||||||
|  |     # Генерируем уникальный отпечаток на основе hostname и даты | ||||||
|  |     fingerprint=$(echo "$(hostname)_$(date +%Y-%m-%d)" | sha256sum 2>/dev/null | cut -d' ' -f1 || echo "$(hostname)_$(date +%Y-%m-%d)") | ||||||
|  |      | ||||||
|  |     # Пробуем разные домены/порты для отправки статистики | ||||||
|  |     for api_url in "https://alr.plemya-x.ru/api/packages/track-install" "http://localhost:3001/api/packages/track-install"; do | ||||||
|  |       curl -s -m 5 -X POST "$api_url" \ | ||||||
|  |         -H "Content-Type: application/json" \ | ||||||
|  |         -H "User-Agent: ALR-InstallScript/1.0" \ | ||||||
|  |         -d "{ | ||||||
|  |           \"packageName\": \"alr-bin\", | ||||||
|  |           \"installType\": \"script\", | ||||||
|  |           \"userAgent\": \"ALR-InstallScript/1.0\", | ||||||
|  |           \"fingerprint\": \"$fingerprint\" | ||||||
|  |         }" >/dev/null 2>&1 | ||||||
|  |       # Если один запрос удался, не пробуем остальные | ||||||
|  |       if [ $? -eq 0 ]; then | ||||||
|  |         break | ||||||
|  |       fi | ||||||
|  |     done | ||||||
|  |   fi | ||||||
|  | } | ||||||
|  |  | ||||||
| if ! command -v curl &>/dev/null; then | if ! command -v curl &>/dev/null; then | ||||||
|   error "Этот скрипт требует команду curl. Пожалуйста, установите её и запустите снова." |   error "Этот скрипт требует команду curl. Пожалуйста, установите её и запустите снова." | ||||||
| fi | fi | ||||||
|  |  | ||||||
|  | # Определение архитектуры системы | ||||||
|  | arch=$(uname -m) | ||||||
|  | case $arch in | ||||||
|  |   x86_64) debArch="amd64"; rpmArch="x86_64" ;; | ||||||
|  |   aarch64) debArch="arm64"; rpmArch="aarch64" ;; | ||||||
|  |   armv7l) debArch="armhf"; rpmArch="armv7hl" ;; | ||||||
|  |   *) error "Неподдерживаемая архитектура: $arch" ;; | ||||||
|  | esac | ||||||
|  |  | ||||||
|  | info "Обнаружена архитектура: $arch" | ||||||
|  |  | ||||||
| pkgFormat="" | pkgFormat="" | ||||||
| pkgMgr="" | pkgMgr="" | ||||||
| if command -v pacman &>/dev/null; then | if command -v pacman &>/dev/null; then | ||||||
| @@ -88,25 +132,50 @@ else | |||||||
| fi | fi | ||||||
|  |  | ||||||
| if [ -z "$noPkgMgr" ]; then | if [ -z "$noPkgMgr" ]; then | ||||||
|   info "Получение списка файлов с https://gitea.plemya-x.ru/Plemya-x/ALR/releases" |   info "Получение списка релизов через API Gitea" | ||||||
|  |  | ||||||
|   # Изменено URL и регулярное выражение для списка файлов |   # Используем API для получения последнего релиза | ||||||
|   pageContent=$(curl -s https://gitea.plemya-x.ru/Plemya-x/ALR/releases) |   releases=$(curl -s "https://gitea.plemya-x.ru/api/v1/repos/Plemya-x/ALR/releases") | ||||||
|  |  | ||||||
|   # Извлечение списка файлов из HTML |   if [ -z "$releases" ] || [ "$releases" = "null" ]; then | ||||||
|   fileList=$(echo "$pageContent" | grep -oP '(?<=href=").*?(?=")' | grep -E 'alr-bin.*\.(pkg.tar.zst|rpm|deb)') |     error "Не удалось получить список релизов. Проверьте соединение с интернетом." | ||||||
|  |   fi | ||||||
|  |  | ||||||
|   echo "Полученный список файлов:" |   # Получаем URL последнего релиза | ||||||
|   echo "$fileList" |   latestReleaseUrl=$(echo "$releases" | grep -o '"browser_download_url":"[^"]*"' | head -1 | cut -d'"' -f4) | ||||||
|  |  | ||||||
|  |   if [ -z "$latestReleaseUrl" ]; then | ||||||
|  |     # Fallback на парсинг HTML если API не работает | ||||||
|  |     warn "API не доступен, пробуем получить список через HTML" | ||||||
|  |     pageContent=$(curl -s https://gitea.plemya-x.ru/Plemya-x/ALR/releases) | ||||||
|  |     fileList=$(echo "$pageContent" | grep -oP '(?<=href=")[^"]*alr-bin[^"]*\.(pkg\.tar\.zst|rpm|deb)' | sed 's|^|https://gitea.plemya-x.ru|') | ||||||
|  |   else | ||||||
|  |     # Получаем список файлов из API | ||||||
|  |     latestReleaseId=$(echo "$releases" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2) | ||||||
|  |     assets=$(curl -s "https://gitea.plemya-x.ru/api/v1/repos/Plemya-x/ALR/releases/$latestReleaseId/assets") | ||||||
|  |     # Фильтруем только пакеты, исключая tar.gz архивы | ||||||
|  |     fileList=$(echo "$assets" | grep -o '"browser_download_url":"[^"]*"' | cut -d'"' -f4 | grep -v '\.tar\.gz$') | ||||||
|  |   fi | ||||||
|  |  | ||||||
|  |   if [ -z "$fileList" ]; then | ||||||
|  |     warn "Не найдены готовые пакеты в последнем релизе" | ||||||
|  |     warn "Возможно, для вашего дистрибутива нужно собрать пакет из исходников" | ||||||
|  |     warn "Инструкции по сборке: https://gitea.plemya-x.ru/Plemya-x/ALR" | ||||||
|  |     error "Не удалось получить список пакетов для загрузки" | ||||||
|  |   fi | ||||||
|  |  | ||||||
|  |   info "Получен список файлов релиза" | ||||||
|  |  | ||||||
|   if [ "$pkgMgr" == "pacman" ]; then |   if [ "$pkgMgr" == "pacman" ]; then | ||||||
|       latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*\.pkg\.tar\.zst' | sort -V | tail -n 1) |       latestFile=$(echo "$fileList" | grep -E "alr-bin.*-(${arch}|any)\.pkg\.tar\.zst" | sort -V | tail -n 1) | ||||||
|   elif [ "$pkgMgr" == "apt" ]; then |   elif [ "$pkgMgr" == "apt" ]; then | ||||||
|       latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*\.amd64\.deb' | sort -V | tail -n 1) |       latestFile=$(echo "$fileList" | grep -E "alr-bin.*_(${debArch}|all)\.deb" | sort -V | tail -n 1) | ||||||
|   elif [[ "$pkgMgr" == "dnf" || "$pkgMgr" == "yum" || "$pkgMgr" == "zypper" ]]; then |   elif [[ "$pkgMgr" == "dnf" || "$pkgMgr" == "yum" || "$pkgMgr" == "zypper" ]]; then | ||||||
|       latestFile=$(printf "%s\n" "${fileList[@]}" | grep -E 'alr-bin-.*\.x86_64\.rpm' | grep -v 'alt[0-9]*' | sort -V | tail -n 1) |       latestFile=$(echo "$fileList" | grep -E "alr-bin.*\.(${rpmArch}|noarch)\.rpm" | grep -v 'alt[0-9]*' | sort -V | tail -n 1) | ||||||
|   elif [ "$pkgMgr" == "apt-get" ]; then |   elif [ "$pkgMgr" == "apt-get" ]; then | ||||||
|       latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*-alt[0-9]+\.x86_64\.rpm' | sort -V | tail -n 1) |       latestFile=$(echo "$fileList" | grep -E "alr-bin.*-alt[0-9]+\.(${rpmArch}|noarch)\.rpm" | sort -V | tail -n 1) | ||||||
|  |   elif [ "$pkgMgr" == "apk" ]; then | ||||||
|  |       latestFile=$(echo "$fileList" | grep -E "alr-bin.*\.apk" | sort -V | tail -n 1) | ||||||
|   else |   else | ||||||
|       error "Не поддерживаемый менеджер пакетов для автоматической установки" |       error "Не поддерживаемый менеджер пакетов для автоматической установки" | ||||||
|   fi |   fi | ||||||
| @@ -119,18 +188,35 @@ if [ -z "$noPkgMgr" ]; then | |||||||
|  |  | ||||||
|   fname="$(mktemp -u -p /tmp "alr.XXXXXXXXXX").${pkgFormat}" |   fname="$(mktemp -u -p /tmp "alr.XXXXXXXXXX").${pkgFormat}" | ||||||
|  |  | ||||||
|   info "Загрузка пакета ALR" |   # Настраиваем trap для очистки временного файла | ||||||
|   curl -o $fname -L "$latestFile" |   trap "rm -f $fname" EXIT | ||||||
|  |  | ||||||
|   if [ ! -f "$fname" ]; then |   info "Загрузка пакета ALR" | ||||||
|       error "Ошибка загрузки пакета ALR" |   info "URL: $latestFile" | ||||||
|  |  | ||||||
|  |   # Загружаем с проверкой кода возврата | ||||||
|  |   if ! curl -f -L -o "$fname" "$latestFile"; then | ||||||
|  |       error "Ошибка загрузки пакета ALR. Проверьте подключение к интернету." | ||||||
|   fi |   fi | ||||||
|  |  | ||||||
|  |   # Проверяем что файл не пустой | ||||||
|  |   if [ ! -s "$fname" ]; then | ||||||
|  |       error "Загруженный файл пустой или поврежден" | ||||||
|  |   fi | ||||||
|  |  | ||||||
|  |   # Показываем размер загруженного файла | ||||||
|  |   fileSize=$(du -h "$fname" | cut -f1) | ||||||
|  |   info "Загружен пакет размером $fileSize" | ||||||
|  |  | ||||||
|   info "Установка пакета ALR" |   info "Установка пакета ALR" | ||||||
|   installPkg "$pkgMgr" "$fname" |   installPkg "$pkgMgr" "$fname" | ||||||
|  |  | ||||||
|  |   # Отправляем статистику установки | ||||||
|  |   trackInstallation | ||||||
|  |  | ||||||
|   info "Очистка" |   info "Очистка" | ||||||
|   rm "$fname" |   rm -f "$fname" | ||||||
|  |   trap - EXIT | ||||||
|  |  | ||||||
|   info "Готово!" |   info "Готово!" | ||||||
| else | else | ||||||
|   | |||||||
							
								
								
									
										38
									
								
								scripts/test-coverage-precommit.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										38
									
								
								scripts/test-coverage-precommit.sh
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | #!/bin/bash | ||||||
|  | # 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/>. | ||||||
|  |  | ||||||
|  | set -e | ||||||
|  |  | ||||||
|  | # Запускаем тесты с покрытием | ||||||
|  | make test-coverage | ||||||
|  |  | ||||||
|  | # coverage.out в .gitignore, не добавляем его | ||||||
|  | # Но если скрипт coverage-badge.sh изменил какие-то файлы (например, README с бейджем), | ||||||
|  | # они будут добавлены | ||||||
|  | CHANGED_FILES=$(git diff --name-only --diff-filter=M | grep -v '\.out$' | grep -v '^coverage' || true) | ||||||
|  |  | ||||||
|  | if [ ! -z "$CHANGED_FILES" ]; then | ||||||
|  |     echo "Test coverage updated the following files:" | ||||||
|  |     echo "$CHANGED_FILES" | ||||||
|  |     # Добавляем только измененные файлы, которые уже отслеживаются | ||||||
|  |     echo "$CHANGED_FILES" | xargs -r git add | ||||||
|  |     echo "Files were updated and staged" | ||||||
|  | fi | ||||||
|  |  | ||||||
|  | echo "Tests completed successfully" | ||||||
|  | # Всегда возвращаем успех если тесты прошли | ||||||
|  | exit 0 | ||||||
| @@ -29,7 +29,6 @@ import ( | |||||||
| 	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/search" | 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/search" | ||||||
| 	"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" | ||||||
| ) | ) | ||||||
| @@ -72,9 +71,6 @@ func SearchCmd() *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 | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										19
									
								
								upgrade.go
									
									
									
									
									
								
							
							
						
						
									
										19
									
								
								upgrade.go
									
									
									
									
									
								
							| @@ -55,9 +55,6 @@ func UpgradeCmd() *cli.Command { | |||||||
| 			}, | 			}, | ||||||
| 		}, | 		}, | ||||||
| 		Action: utils.RootNeededAction(func(c *cli.Context) error { | 		Action: utils.RootNeededAction(func(c *cli.Context) error { | ||||||
| 			if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { |  | ||||||
| 				return err |  | ||||||
| 			} |  | ||||||
|  |  | ||||||
| 			installer, installerClose, err := build.GetSafeInstaller() | 			installer, installerClose, err := build.GetSafeInstaller() | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| @@ -65,9 +62,6 @@ func UpgradeCmd() *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 { | ||||||
| @@ -90,6 +84,19 @@ func UpgradeCmd() *cli.Command { | |||||||
| 			} | 			} | ||||||
| 			defer deps.Defer() | 			defer deps.Defer() | ||||||
|  |  | ||||||
|  | 			// Обновляем систему, если это включено в конфигурации | ||||||
|  | 			if deps.Cfg.UpdateSystemOnUpgrade() { | ||||||
|  | 				slog.Info(gotext.Get("Updating system packages...")) | ||||||
|  | 				err = deps.Manager.UpgradeAll(&manager.Opts{ | ||||||
|  | 					NoConfirm: !c.Bool("interactive"), | ||||||
|  | 					Args:      manager.Args, | ||||||
|  | 				}) | ||||||
|  | 				if err != nil { | ||||||
|  | 					return cliutils.FormatCliExit(gotext.Get("Error updating system packages"), err) | ||||||
|  | 				} | ||||||
|  | 				slog.Info(gotext.Get("System packages updated successfully")) | ||||||
|  | 			} | ||||||
|  |  | ||||||
| 			builder, err := build.NewMainBuilder( | 			builder, err := build.NewMainBuilder( | ||||||
| 				deps.Cfg, | 				deps.Cfg, | ||||||
| 				deps.Manager, | 				deps.Manager, | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user