forked from Plemya-x/ALR
		
	Compare commits
	
		
			38 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 107075e8ef | |||
| 41e3d8119f | |||
| cf804ec66b | |||
| 6773d51caf | |||
| 4a616f2137 | |||
| 9efebbc02a | |||
| ef41d682a1 | |||
| 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 | 
| @@ -19,7 +19,7 @@ name: Pre-commit | ||||
|  | ||||
| on: | ||||
|   push: | ||||
|     branches: [ main ] | ||||
|     branches: [ master ] | ||||
|   pull_request: | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -47,7 +47,7 @@ jobs: | ||||
|  | ||||
|       - name: Prepare for install | ||||
|         run: | | ||||
|           apt-get update && apt-get install -y libcap2-bin bindfs | ||||
|           apt-get update | ||||
|  | ||||
|       - name: Build alr | ||||
|         env: | ||||
| @@ -78,43 +78,57 @@ jobs: | ||||
|           token: ${{ secrets.GITEAPUBLIC }} | ||||
|           path: alr-default | ||||
|  | ||||
|       - name: Update version in alr-bin | ||||
|       - name: Calculate checksum | ||||
|         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/release='[0-9]\+'/release='1'/g" alr-default/alr-bin/alr.sh | ||||
|  | ||||
| #      - name: Install alr | ||||
| #        run: | | ||||
| #          make install | ||||
| # | ||||
| #          # temporary fix | ||||
| #          groupadd wheel | ||||
| #          usermod -aG wheel root | ||||
|           # Обновляем контрольную сумму | ||||
|           sed -i "s/checksums=('[^']*')/checksums=('${{ env.CHECKSUM }}')/g" alr-default/alr-bin/alr.sh | ||||
|  | ||||
| #      - 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+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 | ||||
|       - name: Commit and push changes to alr-default | ||||
|         run: | | ||||
|           cd alr-default | ||||
|           git config user.name "gitea" | ||||
|           git config user.email "admin@plemya-x.ru" | ||||
|           git add . | ||||
|           git commit -m "Обновление версии до ${{ env.VERSION }}" | ||||
|           git add alr-bin/alr.sh | ||||
|           git commit -m "Обновление alr-bin до версии ${{ env.VERSION }}" | ||||
|           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 | ||||
							
								
								
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										9
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -3,11 +3,14 @@ | ||||
| /cmd/alr-api-server/alr-api-server | ||||
| /dist/ | ||||
| /internal/config/version.txt | ||||
| .fleet | ||||
| .idea | ||||
| .gigaide | ||||
| .fleet/ | ||||
| .idea/ | ||||
| .gigaide/ | ||||
|  | ||||
| *.out | ||||
|  | ||||
| e2e-tests/alr | ||||
| CLAUDE.md | ||||
| commit_msg.txt | ||||
| /scripts/.claude/settings.local.json | ||||
| /ALR | ||||
|   | ||||
| @@ -19,13 +19,13 @@ repos: | ||||
|     hooks: | ||||
|       - id: test-coverage | ||||
|         name: Run test coverage | ||||
|         entry: make test-coverage | ||||
|         entry: bash scripts/test-coverage-precommit.sh | ||||
|         language: system | ||||
|         pass_filenames: false | ||||
|  | ||||
|       - id: fmt | ||||
|         name: Format code | ||||
|         entry: make fmt | ||||
|         entry: bash scripts/fmt-precommit.sh | ||||
|         language: system | ||||
|         pass_filenames: false | ||||
|  | ||||
| @@ -37,6 +37,7 @@ repos: | ||||
|  | ||||
|       - id: i18n | ||||
|         name: Update i18n | ||||
|         entry: make i18n | ||||
|         entry: bash scripts/i18n-precommit.sh | ||||
|         language: system | ||||
|         pass_filenames: false | ||||
|         always_run: true | ||||
							
								
								
									
										13
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								Makefile
									
									
									
									
									
								
							| @@ -49,17 +49,12 @@ install: \ | ||||
| $(INSTALLED_BIN): $(BIN) | ||||
| 	install -Dm755 $< $@ | ||||
| ifeq ($(CREATE_SYSTEM_RESOURCES),1) | ||||
| 	setcap cap_setuid,cap_setgid+ep $(INSTALLED_BIN) | ||||
| 	@if id alr >/dev/null 2>&1; then \ | ||||
| 		echo "User 'alr' already exists. Skipping."; \ | ||||
| 	else \ | ||||
| 		useradd -r -s /usr/sbin/nologin alr; \ | ||||
| 	fi | ||||
| 	@for dir in $(ROOT_DIRS); do \ | ||||
| 		install -d -o alr -g alr -m 755 $$dir; \ | ||||
| 		install -d -m 775 $$dir; \ | ||||
| 		chgrp wheel $$dir; \ | ||||
| 	done | ||||
| else | ||||
| 	@echo "Skipping user and root dir creation (CREATE_SYSTEM_RESOURCES=0)" | ||||
| 	@echo "Skipping root dir creation (CREATE_SYSTEM_RESOURCES=0)" | ||||
| endif | ||||
|  | ||||
| $(INSTALLED_BASH_COMPLETION): $(BASH_COMPLETION) | ||||
| @@ -93,7 +88,7 @@ i18n: | ||||
| 	bash scripts/i18n-badge.sh | ||||
|  | ||||
| test-coverage: | ||||
| 	go test ./... -v -coverpkg=./... -coverprofile=coverage.out | ||||
| 	go test -tags=test ./... -v -coverpkg=./... -coverprofile=coverage.out | ||||
| 	bash scripts/coverage-badge.sh | ||||
|  | ||||
| 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 [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 | ||||
| ``` | ||||
| Репозиторий пакетов [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 | ||||
| ``` | ||||
| Репозиторий 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 | ||||
|  | ||||
| ## Спасибы | ||||
|   | ||||
							
								
								
									
										33
									
								
								build.go
									
									
									
									
									
								
							
							
						
						
									
										33
									
								
								build.go
									
									
									
									
									
								
							| @@ -63,7 +63,7 @@ func BuildCmd() *cli.Command { | ||||
| 			}, | ||||
| 		}, | ||||
| 		Action: func(c *cli.Context) error { | ||||
| 			if err := utils.EnuseIsPrivilegedGroupMember(); err != nil { | ||||
| 			if err := utils.CheckUserPrivileges(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| @@ -72,12 +72,6 @@ func BuildCmd() *cli.Command { | ||||
| 				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 | ||||
|  | ||||
| 			deps, err := appbuilder. | ||||
| @@ -156,19 +150,9 @@ func BuildCmd() *cli.Command { | ||||
| 				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() | ||||
| 			if err != nil { | ||||
| @@ -176,9 +160,7 @@ func BuildCmd() *cli.Command { | ||||
| 			} | ||||
| 			defer installerClose() | ||||
|  | ||||
| 			if err := utils.ExitIfCantSetNoNewPrivs(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
|  | ||||
| 			scripter, scripterClose, err := build.GetSafeScriptExecutor() | ||||
| 			if err != nil { | ||||
| @@ -215,6 +197,13 @@ func BuildCmd() *cli.Command { | ||||
|  | ||||
| 			for _, pkg := range res { | ||||
| 				name := filepath.Base(pkg.Path) | ||||
|  | ||||
| 				// Проверяем, существует ли файл перед перемещением | ||||
| 				if _, err := os.Stat(pkg.Path); os.IsNotExist(err) { | ||||
| 					slog.Info("Package file already moved or removed, skipping", "path", pkg.Path) | ||||
| 					continue | ||||
| 				} | ||||
|  | ||||
| 				err = osutils.Move(pkg.Path, filepath.Join(wd, name)) | ||||
| 				if err != nil { | ||||
| 					return cliutils.FormatCliExit(gotext.Get("Error moving the package"), err) | ||||
|   | ||||
| @@ -76,6 +76,7 @@ var configKeys = []string{ | ||||
| 	"autoPull", | ||||
| 	"logLevel", | ||||
| 	"ignorePkgUpdates", | ||||
| 	"updateSystemOnUpgrade", | ||||
| } | ||||
|  | ||||
| func SetConfig() *cli.Command { | ||||
| @@ -137,6 +138,12 @@ func SetConfig() *cli.Command { | ||||
| 					} | ||||
| 				} | ||||
| 				deps.Cfg.System.SetIgnorePkgUpdates(updates) | ||||
| 			case "updateSystemOnUpgrade": | ||||
| 				boolValue, err := strconv.ParseBool(value) | ||||
| 				if err != nil { | ||||
| 					return cliutils.FormatCliExit(gotext.Get("invalid boolean value for %s: %s", key, value), err) | ||||
| 				} | ||||
| 				deps.Cfg.System.SetUpdateSystemOnUpgrade(boolValue) | ||||
| 			case "repo", "repos": | ||||
| 				return cliutils.FormatCliExit(gotext.Get("use 'repo add/remove' commands to manage repositories"), nil) | ||||
| 			default: | ||||
| @@ -206,6 +213,8 @@ func GetConfig() *cli.Command { | ||||
| 				} else { | ||||
| 					fmt.Println(strings.Join(updates, ", ")) | ||||
| 				} | ||||
| 			case "updateSystemOnUpgrade": | ||||
| 				fmt.Println(deps.Cfg.UpdateSystemOnUpgrade()) | ||||
| 			case "repo", "repos": | ||||
| 				repos := deps.Cfg.Repos() | ||||
| 				if len(repos) == 0 { | ||||
|   | ||||
| @@ -45,17 +45,17 @@ func TestE2EIssue130Install(t *testing.T) { | ||||
| 	) | ||||
| 	runMatrixSuite( | ||||
| 		t, | ||||
| 		"alr install {package}+alr-{repo}", | ||||
| 		"alr install {package}+{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)). | ||||
| 			r.Command("sudo", "alr", "in", fmt.Sprintf("foo-pkg+%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")). | ||||
| 			r.Command("sudo", "alr", "in", fmt.Sprintf("bar-pkg+%s", "NOT_REPO_NAME_FOR_E2E_TESTS")). | ||||
| 				ExpectFailure(). | ||||
| 				Run(t) | ||||
| 		}, | ||||
|   | ||||
							
								
								
									
										102
									
								
								fix.go
									
									
									
									
									
								
							
							
						
						
									
										102
									
								
								fix.go
									
									
									
									
									
								
							| @@ -23,6 +23,7 @@ import ( | ||||
| 	"io/fs" | ||||
| 	"log/slog" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"github.com/leonelquinteros/gotext" | ||||
| @@ -33,14 +34,28 @@ import ( | ||||
| 	"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 { | ||||
| 	return &cli.Command{ | ||||
| 		Name:  "fix", | ||||
| 		Usage: gotext.Get("Attempt to fix problems with ALR"), | ||||
| 		Action: func(c *cli.Context) error { | ||||
| 			if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			// Команда выполняется от текущего пользователя | ||||
| 			// При необходимости будет запрошен sudo для удаления файлов root | ||||
|  | ||||
| 			ctx := c.Context | ||||
|  | ||||
| @@ -57,12 +72,18 @@ func FixCmd() *cli.Command { | ||||
|  | ||||
| 			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) | ||||
| 			if err != nil { | ||||
| 				if os.IsNotExist(err) { | ||||
| 					// Директория не существует, просто создадим её позже | ||||
| 					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) | ||||
| @@ -73,19 +94,88 @@ func FixCmd() *cli.Command { | ||||
| 				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 { | ||||
| 					return cliutils.FormatCliExit(gotext.Get("Unable to remove cache item (%s)", entry), err) | ||||
| 						// Если не получилось удалить, пробуем через 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 { | ||||
| 					// Если не получилось удалить, пробуем через 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 и правами 2775 | ||||
| 			err = utils.EnsureTempDirWithRootOwner(tmpDir, 0o2775) | ||||
| 			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, 0o2775) | ||||
| 			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, 0o2775) | ||||
| 			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 { | ||||
| 					group := utils.GetPrivilegedGroup() | ||||
| 					fixCmd := execWithPrivileges("chown", "-R", "root:"+group, 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")) | ||||
|  | ||||
| 			err = os.MkdirAll(paths.CacheDir, 0o755) | ||||
| 			// Создаем директорию кэша с правильными правами | ||||
| 			slog.Info(gotext.Get("Creating cache directory")) | ||||
| 			err = utils.EnsureTempDirWithRootOwner(paths.CacheDir, 0o2775) | ||||
| 			if err != nil { | ||||
| 				return cliutils.FormatCliExit(gotext.Get("Unable to create new cache directory"), err) | ||||
| 			} | ||||
|   | ||||
							
								
								
									
										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"), | ||||
| 					}) | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| } | ||||
|   | ||||
							
								
								
									
										8
									
								
								info.go
									
									
									
									
									
								
							
							
						
						
									
										8
									
								
								info.go
									
									
									
									
									
								
							| @@ -31,7 +31,6 @@ import ( | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" | ||||
| 	appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/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/distro" | ||||
| ) | ||||
| @@ -48,9 +47,6 @@ func InfoCmd() *cli.Command { | ||||
| 			}, | ||||
| 		}, | ||||
| 		BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error { | ||||
| 			if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			ctx := c.Context | ||||
| 			deps, err := appbuilder. | ||||
| @@ -74,9 +70,7 @@ func InfoCmd() *cli.Command { | ||||
| 			return nil | ||||
| 		}), | ||||
| 		Action: func(c *cli.Context) error { | ||||
| 			if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			// Запуск от текущего пользователя | ||||
|  | ||||
| 			args := c.Args() | ||||
| 			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) | ||||
| 			} | ||||
|  | ||||
| 			if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			installer, installerClose, err := build.GetSafeInstaller() | ||||
| 			if err != nil { | ||||
| @@ -61,9 +58,6 @@ func InstallCmd() *cli.Command { | ||||
| 			} | ||||
| 			defer installerClose() | ||||
|  | ||||
| 			if err := utils.ExitIfCantSetNoNewPrivs(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			scripter, scripterClose, err := build.GetSafeScriptExecutor() | ||||
| 			if err != nil { | ||||
| @@ -116,9 +110,6 @@ func InstallCmd() *cli.Command { | ||||
| 			return nil | ||||
| 		}), | ||||
| 		BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error { | ||||
| 			if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			ctx := c.Context | ||||
| 			deps, err := appbuilder. | ||||
|   | ||||
							
								
								
									
										163
									
								
								internal.go
									
									
									
									
									
								
							
							
						
						
									
										163
									
								
								internal.go
									
									
									
									
									
								
							| @@ -17,14 +17,8 @@ | ||||
| package main | ||||
|  | ||||
| import ( | ||||
| 	"bufio" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"log/slog" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"os/user" | ||||
| 	"path/filepath" | ||||
| 	"syscall" | ||||
|  | ||||
| 	"github.com/hashicorp/go-hclog" | ||||
| @@ -36,7 +30,6 @@ import ( | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" | ||||
| 	appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/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/manager" | ||||
| 	"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()) | ||||
|  | ||||
| 			if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			cfg := config.New() | ||||
| 			err := cfg.Load() | ||||
| @@ -92,9 +82,6 @@ func InternalReposCmd() *cli.Command { | ||||
| 		Action: utils.RootNeededAction(func(ctx *cli.Context) error { | ||||
| 			logger.SetupForGoPlugin() | ||||
|  | ||||
| 			if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			deps, err := appbuilder. | ||||
| 				New(ctx.Context). | ||||
| @@ -129,16 +116,7 @@ func InternalInstallCmd() *cli.Command { | ||||
| 		Action: func(c *cli.Context) error { | ||||
| 			logger.SetupForGoPlugin() | ||||
|  | ||||
| 			if err := utils.EnsureIsAlrUser(); err != nil { | ||||
| 				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) | ||||
| 			} | ||||
| 			// Запуск от текущего пользователя, повышение прав будет через sudo при необходимости | ||||
|  | ||||
| 			deps, err := appbuilder. | ||||
| 				New(c.Context). | ||||
| @@ -175,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/config" | ||||
| 	"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/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" | ||||
| @@ -319,9 +320,9 @@ func (b *Builder) BuildPackage( | ||||
| 	} | ||||
|  | ||||
| 	var builtDeps []*BuiltDep | ||||
| 	var remainingVars []*alrsh.Package | ||||
|  | ||||
| 	if !input.opts.Clean { | ||||
| 		var remainingVars []*alrsh.Package | ||||
| 		for _, vars := range varsOfPackages { | ||||
| 			builtPkgPath, ok, err := b.cacheExecutor.CheckForBuiltPackage(ctx, input, vars) | ||||
| 			if err != nil { | ||||
| @@ -330,6 +331,7 @@ func (b *Builder) BuildPackage( | ||||
| 			if ok { | ||||
| 				builtDeps = append(builtDeps, &BuiltDep{ | ||||
| 					Path: builtPkgPath, | ||||
| 					Name: vars.Name, | ||||
| 				}) | ||||
| 			} else { | ||||
| 				remainingVars = append(remainingVars, vars) | ||||
| @@ -337,8 +339,12 @@ func (b *Builder) BuildPackage( | ||||
| 		} | ||||
|  | ||||
| 		if len(remainingVars) == 0 { | ||||
| 			slog.Info(gotext.Get("Using cached package"), "name", basePkg) | ||||
| 			return builtDeps, nil | ||||
| 		} | ||||
| 		 | ||||
| 		// Обновляем varsOfPackages только теми пакетами, которые нужно собрать | ||||
| 		varsOfPackages = remainingVars | ||||
| 	} | ||||
|  | ||||
| 	slog.Debug("ViewScript") | ||||
| @@ -401,11 +407,21 @@ func (b *Builder) BuildPackage( | ||||
|  | ||||
| 	// We filter so as not to re-build what has already been built at the `installBuildDeps` stage. | ||||
| 	var filteredDepends []string | ||||
| 	 | ||||
| 	// Создаем набор подпакетов текущего мультипакета для исключения циклических зависимостей | ||||
| 	currentPackageNames := make(map[string]struct{}) | ||||
| 	for _, pkg := range input.packages { | ||||
| 		currentPackageNames[pkg] = struct{}{} | ||||
| 	} | ||||
| 	 | ||||
| 	for _, d := range depends { | ||||
| 		if _, found := depNames[d]; !found { | ||||
| 			// Исключаем зависимости, которые являются подпакетами текущего мультипакета | ||||
| 			if _, isCurrentPackage := currentPackageNames[d]; !isCurrentPackage { | ||||
| 				filteredDepends = append(filteredDepends, d) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	slog.Debug("BuildALRDeps") | ||||
| 	newBuiltDeps, repoDeps, err := b.BuildALRDeps(ctx, input, filteredDepends) | ||||
| @@ -528,6 +544,13 @@ func (b *Builder) InstallALRPackages( | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		 | ||||
| 		// Отслеживание установки ALR пакетов | ||||
| 		for _, dep := range res { | ||||
| 			if stats.ShouldTrackPackage(dep.Name) { | ||||
| 				stats.TrackInstallation(ctx, dep.Name, "upgrade") | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| @@ -552,11 +575,13 @@ func (b *Builder) BuildALRDeps( | ||||
| 		repoDeps = notFound | ||||
|  | ||||
| 		// Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез | ||||
| 		pkgs := cliutils.FlattenPkgs( | ||||
| 		// Для зависимостей указываем isDependency = true | ||||
| 		pkgs := cliutils.FlattenPkgsWithContext( | ||||
| 			ctx, | ||||
| 			found, | ||||
| 			"install", | ||||
| 			input.BuildOpts().Interactive, | ||||
| 			true, | ||||
| 		) | ||||
| 		type item struct { | ||||
| 			pkg      *alrsh.Package | ||||
| @@ -691,6 +716,13 @@ func (i *Builder) InstallPkgs( | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		 | ||||
| 		// Отслеживание установки локальных пакетов | ||||
| 		for _, dep := range builtDeps { | ||||
| 			if stats.ShouldTrackPackage(dep.Name) { | ||||
| 				stats.TrackInstallation(ctx, dep.Name, "install") | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if len(repoDeps) > 0 { | ||||
| @@ -700,6 +732,13 @@ func (i *Builder) InstallPkgs( | ||||
| 		if err != nil { | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		 | ||||
| 		// Отслеживание установки пакетов из репозитория | ||||
| 		for _, pkg := range repoDeps { | ||||
| 			if stats.ShouldTrackPackage(pkg) { | ||||
| 				stats.TrackInstallation(ctx, pkg, "install") | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return builtDeps, nil | ||||
|   | ||||
| @@ -44,9 +44,9 @@ var HandshakeConfig = plugin.HandshakeConfig{ | ||||
|  | ||||
| func setCommonCmdEnv(cmd *exec.Cmd) { | ||||
| 	cmd.Env = []string{ | ||||
| 		"HOME=/var/cache/alr", | ||||
| 		"LOGNAME=alr", | ||||
| 		"USER=alr", | ||||
| 		"HOME=" + os.Getenv("HOME"), | ||||
| 		"LOGNAME=" + os.Getenv("USER"), | ||||
| 		"USER=" + os.Getenv("USER"), | ||||
| 		"PATH=/usr/bin:/bin:/usr/local/bin", | ||||
| 	} | ||||
| 	for _, env := range os.Environ() { | ||||
| @@ -102,9 +102,7 @@ func getSafeExecutor[T any](subCommand, pluginName string) (T, func(), error) { | ||||
| 		Cmd:             cmd, | ||||
| 		Logger:          logger.GetHCLoggerAdapter(), | ||||
| 		SkipHostEnv:     true, | ||||
| 		UnixSocketConfig: &plugin.UnixSocketConfig{ | ||||
| 			Group: "alr", | ||||
| 		}, | ||||
| 		UnixSocketConfig: &plugin.UnixSocketConfig{}, | ||||
| 		SyncStderr: os.Stderr, | ||||
| 	}) | ||||
| 	rpcClient, err := client.Client() | ||||
|   | ||||
| @@ -167,15 +167,30 @@ func (e *LocalScriptExecutor) ExecuteSecondPass( | ||||
| 		pkgName := packager.ConventionalFileName(pkgInfo) // Получаем имя файла пакета | ||||
| 		pkgPath := filepath.Join(dirs.BaseDir, pkgName)   // Определяем путь к пакету | ||||
|  | ||||
| 		slog.Info("Creating package file", "path", pkgPath, "name", pkgName) | ||||
|  | ||||
| 		pkgFile, err := os.Create(pkgPath) | ||||
| 		if err != nil { | ||||
| 			slog.Error("Failed to create package file", "path", pkgPath, "error", err) | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		defer pkgFile.Close() | ||||
|  | ||||
| 		slog.Info("Packaging with nfpm", "format", pkgFormat) | ||||
| 		err = packager.Package(pkgInfo, pkgFile) | ||||
| 		if err != nil { | ||||
| 			slog.Error("Failed to create package", "path", pkgPath, "error", err) | ||||
| 			return nil, err | ||||
| 		} | ||||
|  | ||||
| 		err = packager.Package(pkgInfo, pkgFile) | ||||
| 		if err != nil { | ||||
| 		slog.Info("Package created successfully", "path", pkgPath) | ||||
|  | ||||
| 		// Проверяем, что файл действительно существует | ||||
| 		if _, err := os.Stat(pkgPath); err != nil { | ||||
| 			slog.Error("Package file not found after creation", "path", pkgPath, "error", err) | ||||
| 			return nil, err | ||||
| 		} | ||||
| 		slog.Info("Package file verified to exist", "path", pkgPath) | ||||
|  | ||||
| 		builtDeps = append(builtDeps, &BuiltDep{ | ||||
| 			Name: vars.Name, | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import ( | ||||
| 	"os" | ||||
| 	"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/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) | ||||
| 		if err != nil { | ||||
|   | ||||
| @@ -19,6 +19,7 @@ package build | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"io" | ||||
| 	"log/slog" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
| 	"regexp" | ||||
| @@ -40,6 +41,7 @@ import ( | ||||
| 	"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/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/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" | ||||
| @@ -47,15 +49,31 @@ import ( | ||||
|  | ||||
| // Функция prepareDirs подготавливает директории для сборки. | ||||
| func prepareDirs(dirs types.Directories) error { | ||||
| 	err := os.RemoveAll(dirs.BaseDir) // Удаляем базовую директорию, если она существует | ||||
| 	// Удаляем только директории источников и упаковки, не трогаем файлы пакетов в BaseDir | ||||
| 	err := os.RemoveAll(dirs.SrcDir) | ||||
| 	if err != nil { | ||||
| 		slog.Debug("Failed to remove src directory", "path", dirs.SrcDir, "error", err) | ||||
| 	} | ||||
|  | ||||
| 	err = os.RemoveAll(dirs.PkgDir) | ||||
| 	if err != nil { | ||||
| 		slog.Debug("Failed to remove pkg directory", "path", dirs.PkgDir, "error", err) | ||||
| 	} | ||||
|  | ||||
| 	// Создаем базовую директорию для пакета с setgid битом | ||||
| 	err = utils.EnsureTempDirWithRootOwner(dirs.BaseDir, 0o2775) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	err = os.MkdirAll(dirs.SrcDir, 0o755) // Создаем директорию для источников | ||||
|  | ||||
| 	// Создаем директории с правильным владельцем для /tmp/alr с setgid битом | ||||
| 	err = utils.EnsureTempDirWithRootOwner(dirs.SrcDir, 0o2775) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return os.MkdirAll(dirs.PkgDir, 0o755) // Создаем директорию для пакетов | ||||
|  | ||||
| 	// Создаем директорию для пакетов с setgid битом | ||||
| 	return utils.EnsureTempDirWithRootOwner(dirs.PkgDir, 0o2775) | ||||
| } | ||||
|  | ||||
| // Функция buildContents создает секцию содержимого пакета, которая содержит файлы, | ||||
| @@ -160,15 +178,16 @@ func normalizeContents(contents []*files.Content) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| var RegexpALRPackageName = regexp.MustCompile(`^(?P<package>[^+]+)\+alr-(?P<repo>.+)$`) | ||||
| var RegexpALRPackageName = regexp.MustCompile(`^(?P<package>[^+]+)\+(?P<repo>.+)$`) | ||||
|  | ||||
| func getBasePkgInfo(vars *alrsh.Package, input interface { | ||||
| 	RepositoryProvider | ||||
| 	OsInfoProvider | ||||
| }, | ||||
| ) *nfpm.Info { | ||||
| 	repo := input.Repository() | ||||
| 	return &nfpm.Info{ | ||||
| 		Name:    fmt.Sprintf("%s+alr-%s", vars.Name, input.Repository()), | ||||
| 		Name:    fmt.Sprintf("%s+%s", vars.Name, repo), | ||||
| 		Arch:    cpu.Arch(), | ||||
| 		Version: vars.Version, | ||||
| 		Release: overrides.ReleasePlatformSpecific(vars.Release, input.OSRelease()), | ||||
|   | ||||
							
								
								
									
										158
									
								
								internal/build/utils_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								internal/build/utils_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | ||||
| // 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 ( | ||||
| 	"testing" | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" | ||||
| ) | ||||
|  | ||||
| type mockInput struct { | ||||
| 	repo    string | ||||
| 	osInfo  *distro.OSRelease | ||||
| } | ||||
|  | ||||
| func (m *mockInput) Repository() string { | ||||
| 	return m.repo | ||||
| } | ||||
|  | ||||
| func (m *mockInput) OSRelease() *distro.OSRelease { | ||||
| 	return m.osInfo | ||||
| } | ||||
|  | ||||
| func TestGetBasePkgInfo(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name         string | ||||
| 		packageName  string | ||||
| 		repoName     string | ||||
| 		expectedName string | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:         "обычный репозиторий", | ||||
| 			packageName:  "test-package", | ||||
| 			repoName:     "default", | ||||
| 			expectedName: "test-package+default", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:         "репозиторий с alr- префиксом", | ||||
| 			packageName:  "test-package", | ||||
| 			repoName:     "alr-default", | ||||
| 			expectedName: "test-package+alr-default", | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:         "репозиторий с двойным alr- префиксом", | ||||
| 			packageName:  "test-package", | ||||
| 			repoName:     "alr-alr-repo", | ||||
| 			expectedName: "test-package+alr-alr-repo", | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			pkg := &alrsh.Package{ | ||||
| 				Name:    tt.packageName, | ||||
| 				Version: "1.0.0", | ||||
| 				Release: 1, | ||||
| 			} | ||||
|  | ||||
| 			input := &mockInput{ | ||||
| 				repo: tt.repoName, | ||||
| 				osInfo: &distro.OSRelease{ | ||||
| 					ID: "test", | ||||
| 				}, | ||||
| 			} | ||||
|  | ||||
| 			info := getBasePkgInfo(pkg, input) | ||||
|  | ||||
| 			if info.Name != tt.expectedName { | ||||
| 				t.Errorf("getBasePkgInfo() имя пакета = %v, ожидается %v", info.Name, tt.expectedName) | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestRegexpALRPackageName(t *testing.T) { | ||||
| 	tests := []struct { | ||||
| 		name         string | ||||
| 		packageName  string | ||||
| 		expectedPkg  string | ||||
| 		expectedRepo string | ||||
| 		shouldMatch  bool | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name:         "новый формат - обычный репозиторий", | ||||
| 			packageName:  "test-package+default", | ||||
| 			expectedPkg:  "test-package", | ||||
| 			expectedRepo: "default", | ||||
| 			shouldMatch:  true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:         "новый формат - alr-default репозиторий", | ||||
| 			packageName:  "test-package+alr-default", | ||||
| 			expectedPkg:  "test-package", | ||||
| 			expectedRepo: "alr-default", | ||||
| 			shouldMatch:  true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:         "новый формат - двойной alr- префикс", | ||||
| 			packageName:  "test-package+alr-alr-repo", | ||||
| 			expectedPkg:  "test-package", | ||||
| 			expectedRepo: "alr-alr-repo", | ||||
| 			shouldMatch:  true, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "некорректный формат - без плюса", | ||||
| 			packageName: "test-package", | ||||
| 			shouldMatch: false, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name:        "некорректный формат - пустое имя пакета", | ||||
| 			packageName: "+repo", | ||||
| 			shouldMatch: false, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	for _, tt := range tests { | ||||
| 		t.Run(tt.name, func(t *testing.T) { | ||||
| 			matches := RegexpALRPackageName.FindStringSubmatch(tt.packageName) | ||||
|  | ||||
| 			if tt.shouldMatch { | ||||
| 				if matches == nil { | ||||
| 					t.Errorf("RegexpALRPackageName должен найти совпадение для %q", tt.packageName) | ||||
| 					return | ||||
| 				} | ||||
|  | ||||
| 				packageName := matches[RegexpALRPackageName.SubexpIndex("package")] | ||||
| 				repoName := matches[RegexpALRPackageName.SubexpIndex("repo")] | ||||
|  | ||||
| 				if packageName != tt.expectedPkg { | ||||
| 					t.Errorf("RegexpALRPackageName извлеченное имя пакета = %v, ожидается %v", packageName, tt.expectedPkg) | ||||
| 				} | ||||
|  | ||||
| 				if repoName != tt.expectedRepo { | ||||
| 					t.Errorf("RegexpALRPackageName извлеченное имя репозитория = %v, ожидается %v", repoName, tt.expectedRepo) | ||||
| 				} | ||||
| 			} else { | ||||
| 				if matches != nil { | ||||
| 					t.Errorf("RegexpALRPackageName не должен найти совпадение для %q", tt.packageName) | ||||
| 				} | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -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 | ||||
| // 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 { | ||||
| 	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 | ||||
| 	for _, pkgs := range found { | ||||
| 		if len(pkgs) > 1 && interactive { | ||||
| 		if len(pkgs) > 1 { | ||||
| 			// Проверяем, являются ли пакеты подпакетами одного мультипакета | ||||
| 			if isMultiPackage(pkgs) && verb == "install" { | ||||
| 				// Для мультипакетов при установке ВСЕГДА берем все подпакеты без выбора | ||||
| 				// Это правильное поведение как для прямой установки, так и для зависимостей | ||||
| 				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 if len(pkgs) == 1 || !interactive { | ||||
| 			} else { | ||||
| 				// Если не интерактивный режим - берем первый | ||||
| 				outPkgs = append(outPkgs, pkgs[0]) | ||||
| 			} | ||||
| 		} else { | ||||
| 			// Если только один пакет - берем его | ||||
| 			outPkgs = append(outPkgs, pkgs[0]) | ||||
| 		} | ||||
| 	} | ||||
| 	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. | ||||
| func PkgPrompt(ctx context.Context, options []alrsh.Package, verb string, interactive bool) (alrsh.Package, error) { | ||||
| 	if !interactive { | ||||
|   | ||||
| @@ -21,11 +21,14 @@ package config | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"github.com/goccy/go-yaml" | ||||
| 	"github.com/knadh/koanf/providers/confmap" | ||||
| 	"github.com/knadh/koanf/v2" | ||||
| 	ktoml "github.com/knadh/koanf/parsers/toml/v2" | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" | ||||
| @@ -55,7 +58,13 @@ func defaultConfigKoanf() *koanf.Koanf { | ||||
| 		"ignorePkgUpdates": []string{}, | ||||
| 		"logLevel":         "info", | ||||
| 		"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 { | ||||
| 		panic(k) | ||||
| @@ -98,8 +107,20 @@ func (c *ALRConfig) Load() error { | ||||
| 	c.paths.UserConfigPath = constants.SystemConfigPath | ||||
| 	c.paths.CacheDir = constants.SystemCachePath | ||||
| 	c.paths.RepoDir = filepath.Join(c.paths.CacheDir, "repo") | ||||
| 	c.paths.PkgsDir = filepath.Join(c.paths.CacheDir, "pkgs") | ||||
| 	c.paths.DBPath = filepath.Join(c.paths.CacheDir, "db") | ||||
| 	c.paths.PkgsDir = filepath.Join(constants.TempDir, "pkgs")  // Перемещаем в /tmp/alr/pkgs | ||||
| 	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 | ||||
| } | ||||
| @@ -112,6 +133,126 @@ func (c *ALRConfig) ToYAML() (string, error) { | ||||
| 	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) PagerStyle() string          { return c.cfg.PagerStyle } | ||||
| 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) LogLevel() string            { return c.cfg.LogLevel } | ||||
| func (c *ALRConfig) UseRootCmd() bool            { return c.cfg.UseRootCmd } | ||||
| func (c *ALRConfig) UpdateSystemOnUpgrade() bool { return c.cfg.UpdateSystemOnUpgrade } | ||||
| func (c *ALRConfig) GetPaths() *Paths            { return c.paths } | ||||
|   | ||||
| @@ -142,3 +142,10 @@ func (c *SystemConfig) SetRepos(v []types.Repo) { | ||||
| 		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 ( | ||||
| 	SystemConfigPath = "/etc/alr/alr.toml" | ||||
| 	SystemCachePath  = "/var/cache/alr" | ||||
| 	AlrRunDir        = "/var/run/alr" | ||||
| 	PrivilegedGroup  = "wheel" | ||||
| 	TempDir          = "/tmp/alr" | ||||
| 	// PrivilegedGroup - устарело, используйте GetPrivilegedGroup() | ||||
| 	PrivilegedGroup  = "wheel" // оставлено для обратной совместимости | ||||
| ) | ||||
|   | ||||
| @@ -21,7 +21,10 @@ package db | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"fmt" | ||||
| 	"log/slog" | ||||
| 	"os" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"github.com/leonelquinteros/gotext" | ||||
| 	_ "modernc.org/sqlite" | ||||
| @@ -54,6 +57,19 @@ func New(config Config) *Database { | ||||
|  | ||||
| func (d *Database) Connect() error { | ||||
| 	dsn := d.config.GetPaths().DBPath | ||||
| 	 | ||||
| 	// Проверяем директорию для БД | ||||
| 	dbDir := filepath.Dir(dsn) | ||||
| 	if _, err := os.Stat(dbDir); err != nil { | ||||
| 		if os.IsNotExist(err) { | ||||
| 			// Директория не существует - не пытаемся создать | ||||
| 			// Пользователь должен использовать alr fix для создания системных каталогов | ||||
| 			return fmt.Errorf("cache directory does not exist, please run 'sudo alr fix' to create it") | ||||
| 		} else { | ||||
| 			return fmt.Errorf("failed to check database directory: %w", err) | ||||
| 		} | ||||
| 	} | ||||
| 	 | ||||
| 	engine, err := xorm.NewEngine("sqlite", dsn) | ||||
| 	// engine.SetLogLevel(log.LOG_DEBUG) | ||||
| 	// 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) { | ||||
| 	cmd := exec.Command("dpkg-query", "-l", pkg) | ||||
| 	cmd := exec.Command("dpkg-query", "-f", "${Status}", "-W", pkg) | ||||
| 	output, err := cmd.CombinedOutput() | ||||
| 	if err != nil { | ||||
| 		if exitErr, ok := err.(*exec.ExitError); ok { | ||||
| 			// Exit code 1 means the package is not installed | ||||
| 			// Код выхода 1 означает что пакет не найден | ||||
| 			if exitErr.ExitCode() == 1 { | ||||
| 				return false, nil | ||||
| 			} | ||||
| 		} | ||||
| 		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 | ||||
|  | ||||
| import "os/exec" | ||||
| import ( | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| ) | ||||
|  | ||||
| type CommonPackageManager struct { | ||||
| 	noConfirmArg string | ||||
| } | ||||
|  | ||||
| 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, args...) | ||||
|  | ||||
|   | ||||
| @@ -47,9 +47,9 @@ func (rs *Repos) FindPkgs(ctx context.Context, pkgs []string) (map[string][]alrs | ||||
| 			name := parts[1] | ||||
| 			result, err = rs.db.GetPkgs(ctx, "name = ? AND repository = ?", name, repo) | ||||
|  | ||||
| 		case strings.Contains(pkgName, "+alr-"): | ||||
| 			// pkg+alr-repo | ||||
| 			parts := strings.SplitN(pkgName, "+alr-", 2) | ||||
| 		case strings.Contains(pkgName, "+"): | ||||
| 			// pkg+repo | ||||
| 			parts := strings.SplitN(pkgName, "+", 2) | ||||
| 			name := parts[0] | ||||
| 			repo := parts[1] | ||||
| 			result, err = rs.db.GetPkgs(ctx, "name = ? AND repository = ?", name, repo) | ||||
| @@ -60,6 +60,13 @@ func (rs *Repos) FindPkgs(ctx context.Context, pkgs []string) (map[string][]alrs | ||||
| 				return nil, nil, fmt.Errorf("FindPkgs: get by provides: %w", err) | ||||
| 			} | ||||
|  | ||||
| 			if len(result) == 0 { | ||||
| 				result, err = rs.db.GetPkgs(ctx, "basepkg_name = ?", pkgName) | ||||
| 				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) | ||||
| 			} | ||||
|   | ||||
| @@ -177,3 +177,333 @@ func filesFindCmd(hc interp.HandlerContext, cmd string, args []string) error { | ||||
|  | ||||
| 	return outputFiles(hc, foundFiles) | ||||
| } | ||||
|  | ||||
| func filesFindBinCmd(hc interp.HandlerContext, cmd string, args []string) error { | ||||
| 	namePattern := "*" | ||||
| 	if len(args) > 0 { | ||||
| 		namePattern = args[0] | ||||
| 	} | ||||
|  | ||||
| 	binPath := "./usr/bin/" | ||||
| 	realPath := path.Join(hc.Dir, binPath) | ||||
|  | ||||
| 	if err := validateDir(realPath, "files-find-bin"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var binFiles []string | ||||
| 	err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if !info.IsDir() && matchNamePattern(info.Name(), namePattern) { | ||||
| 			relPath, relErr := makeRelativePath(hc.Dir, p) | ||||
| 			if relErr != nil { | ||||
| 				return relErr | ||||
| 			} | ||||
| 			binFiles = append(binFiles, relPath) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("files-find-bin: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return outputFiles(hc, binFiles) | ||||
| } | ||||
|  | ||||
| func filesFindLibCmd(hc interp.HandlerContext, cmd string, args []string) error { | ||||
| 	namePattern := "*" | ||||
| 	if len(args) > 0 { | ||||
| 		namePattern = args[0] | ||||
| 	} | ||||
|  | ||||
| 	libPaths := []string{"./usr/lib/", "./usr/lib64/"} | ||||
| 	var libFiles []string | ||||
|  | ||||
| 	for _, libPath := range libPaths { | ||||
| 		realPath := path.Join(hc.Dir, libPath) | ||||
| 		if _, err := os.Stat(realPath); os.IsNotExist(err) { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error { | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			if !info.IsDir() && matchNamePattern(info.Name(), namePattern) { | ||||
| 				relPath, relErr := makeRelativePath(hc.Dir, p) | ||||
| 				if relErr != nil { | ||||
| 					return relErr | ||||
| 				} | ||||
| 				libFiles = append(libFiles, relPath) | ||||
| 			} | ||||
| 			return nil | ||||
| 		}) | ||||
| 		if err != nil { | ||||
| 			return fmt.Errorf("files-find-lib: %w", err) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return outputFiles(hc, libFiles) | ||||
| } | ||||
|  | ||||
| func filesFindIncludeCmd(hc interp.HandlerContext, cmd string, args []string) error { | ||||
| 	namePattern := "*" | ||||
| 	if len(args) > 0 { | ||||
| 		namePattern = args[0] | ||||
| 	} | ||||
|  | ||||
| 	includePath := "./usr/include/" | ||||
| 	realPath := path.Join(hc.Dir, includePath) | ||||
|  | ||||
| 	if err := validateDir(realPath, "files-find-include"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var includeFiles []string | ||||
| 	err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if !info.IsDir() && matchNamePattern(info.Name(), namePattern) { | ||||
| 			relPath, relErr := makeRelativePath(hc.Dir, p) | ||||
| 			if relErr != nil { | ||||
| 				return relErr | ||||
| 			} | ||||
| 			includeFiles = append(includeFiles, relPath) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("files-find-include: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return outputFiles(hc, includeFiles) | ||||
| } | ||||
|  | ||||
| func filesFindShareCmd(hc interp.HandlerContext, cmd string, args []string) error { | ||||
| 	namePattern := "*" | ||||
| 	sharePath := "./usr/share/" | ||||
|  | ||||
| 	if len(args) > 0 { | ||||
| 		if len(args) == 1 { | ||||
| 			sharePath = "./usr/share/" + args[0] + "/" | ||||
| 		} else { | ||||
| 			sharePath = "./usr/share/" + args[0] + "/" | ||||
| 			namePattern = args[1] | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	realPath := path.Join(hc.Dir, sharePath) | ||||
|  | ||||
| 	if err := validateDir(realPath, "files-find-share"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var shareFiles []string | ||||
| 	err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if !info.IsDir() && matchNamePattern(info.Name(), namePattern) { | ||||
| 			relPath, relErr := makeRelativePath(hc.Dir, p) | ||||
| 			if relErr != nil { | ||||
| 				return relErr | ||||
| 			} | ||||
| 			shareFiles = append(shareFiles, relPath) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("files-find-share: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return outputFiles(hc, shareFiles) | ||||
| } | ||||
|  | ||||
| func filesFindManCmd(hc interp.HandlerContext, cmd string, args []string) error { | ||||
| 	namePattern := "*" | ||||
| 	manSection := "*" | ||||
|  | ||||
| 	if len(args) > 0 { | ||||
| 		if len(args) == 1 { | ||||
| 			manSection = args[0] | ||||
| 		} else { | ||||
| 			manSection = args[0] | ||||
| 			namePattern = args[1] | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	manPath := "./usr/share/man/man" + manSection + "/" | ||||
| 	realPath := path.Join(hc.Dir, manPath) | ||||
|  | ||||
| 	if err := validateDir(realPath, "files-find-man"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var manFiles []string | ||||
| 	err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if !info.IsDir() && matchNamePattern(info.Name(), namePattern) { | ||||
| 			relPath, relErr := makeRelativePath(hc.Dir, p) | ||||
| 			if relErr != nil { | ||||
| 				return relErr | ||||
| 			} | ||||
| 			manFiles = append(manFiles, relPath) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("files-find-man: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return outputFiles(hc, manFiles) | ||||
| } | ||||
|  | ||||
| func filesFindConfigCmd(hc interp.HandlerContext, cmd string, args []string) error { | ||||
| 	namePattern := "*" | ||||
| 	if len(args) > 0 { | ||||
| 		namePattern = args[0] | ||||
| 	} | ||||
|  | ||||
| 	configPath := "./etc/" | ||||
| 	realPath := path.Join(hc.Dir, configPath) | ||||
|  | ||||
| 	if err := validateDir(realPath, "files-find-config"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var configFiles []string | ||||
| 	err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if !info.IsDir() && matchNamePattern(info.Name(), namePattern) { | ||||
| 			relPath, relErr := makeRelativePath(hc.Dir, p) | ||||
| 			if relErr != nil { | ||||
| 				return relErr | ||||
| 			} | ||||
| 			configFiles = append(configFiles, relPath) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("files-find-config: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return outputFiles(hc, configFiles) | ||||
| } | ||||
|  | ||||
| func filesFindSystemdCmd(hc interp.HandlerContext, cmd string, args []string) error { | ||||
| 	namePattern := "*" | ||||
| 	if len(args) > 0 { | ||||
| 		namePattern = args[0] | ||||
| 	} | ||||
|  | ||||
| 	systemdPath := "./usr/lib/systemd/system/" | ||||
| 	realPath := path.Join(hc.Dir, systemdPath) | ||||
|  | ||||
| 	if err := validateDir(realPath, "files-find-systemd"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var systemdFiles []string | ||||
| 	err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if !info.IsDir() && matchNamePattern(info.Name(), namePattern) { | ||||
| 			relPath, relErr := makeRelativePath(hc.Dir, p) | ||||
| 			if relErr != nil { | ||||
| 				return relErr | ||||
| 			} | ||||
| 			systemdFiles = append(systemdFiles, relPath) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("files-find-systemd: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return outputFiles(hc, systemdFiles) | ||||
| } | ||||
|  | ||||
| func filesFindSystemdUserCmd(hc interp.HandlerContext, cmd string, args []string) error { | ||||
| 	namePattern := "*" | ||||
| 	if len(args) > 0 { | ||||
| 		namePattern = args[0] | ||||
| 	} | ||||
|  | ||||
| 	systemdUserPath := "./usr/lib/systemd/user/" | ||||
| 	realPath := path.Join(hc.Dir, systemdUserPath) | ||||
|  | ||||
| 	if err := validateDir(realPath, "files-find-systemd-user"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var systemdUserFiles []string | ||||
| 	err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if !info.IsDir() && matchNamePattern(info.Name(), namePattern) { | ||||
| 			relPath, relErr := makeRelativePath(hc.Dir, p) | ||||
| 			if relErr != nil { | ||||
| 				return relErr | ||||
| 			} | ||||
| 			systemdUserFiles = append(systemdUserFiles, relPath) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("files-find-systemd-user: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return outputFiles(hc, systemdUserFiles) | ||||
| } | ||||
|  | ||||
| func filesFindLicenseCmd(hc interp.HandlerContext, cmd string, args []string) error { | ||||
| 	namePattern := "*" | ||||
| 	if len(args) > 0 { | ||||
| 		namePattern = args[0] | ||||
| 	} | ||||
|  | ||||
| 	licensePath := "./usr/share/licenses/" | ||||
| 	realPath := path.Join(hc.Dir, licensePath) | ||||
|  | ||||
| 	if err := validateDir(realPath, "files-find-license"); err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	var licenseFiles []string | ||||
| 	err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
|  | ||||
| 		if !info.IsDir() && matchNamePattern(info.Name(), namePattern) { | ||||
| 			relPath, relErr := makeRelativePath(hc.Dir, p) | ||||
| 			if relErr != nil { | ||||
| 				return relErr | ||||
| 			} | ||||
| 			licenseFiles = append(licenseFiles, relPath) | ||||
| 		} | ||||
| 		return nil | ||||
| 	}) | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("files-find-license: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	return outputFiles(hc, licenseFiles) | ||||
| } | ||||
|   | ||||
| @@ -59,6 +59,15 @@ var Helpers = handlers.ExecFuncs{ | ||||
| 	"files-find":             filesFindCmd, | ||||
| 	"files-find-lang":        filesFindLangCmd, | ||||
| 	"files-find-doc":         filesFindDocCmd, | ||||
| 	"files-find-bin":         filesFindBinCmd, | ||||
| 	"files-find-lib":         filesFindLibCmd, | ||||
| 	"files-find-include":     filesFindIncludeCmd, | ||||
| 	"files-find-share":       filesFindShareCmd, | ||||
| 	"files-find-man":         filesFindManCmd, | ||||
| 	"files-find-config":      filesFindConfigCmd, | ||||
| 	"files-find-systemd":     filesFindSystemdCmd, | ||||
| 	"files-find-systemd-user": filesFindSystemdUserCmd, | ||||
| 	"files-find-license":     filesFindLicenseCmd, | ||||
| } | ||||
|  | ||||
| // Restricted contains restricted read-only helper commands | ||||
| @@ -68,6 +77,15 @@ var Restricted = handlers.ExecFuncs{ | ||||
| 	"files-find":             filesFindCmd, | ||||
| 	"files-find-lang":        filesFindLangCmd, | ||||
| 	"files-find-doc":         filesFindDocCmd, | ||||
| 	"files-find-bin":         filesFindBinCmd, | ||||
| 	"files-find-lib":         filesFindLibCmd, | ||||
| 	"files-find-include":     filesFindIncludeCmd, | ||||
| 	"files-find-share":       filesFindShareCmd, | ||||
| 	"files-find-man":         filesFindManCmd, | ||||
| 	"files-find-config":      filesFindConfigCmd, | ||||
| 	"files-find-systemd":     filesFindSystemdCmd, | ||||
| 	"files-find-systemd-user": filesFindSystemdUserCmd, | ||||
| 	"files-find-license":     filesFindLicenseCmd, | ||||
| } | ||||
|  | ||||
| func installHelperCmd(prefix string, perms os.FileMode) handlers.ExecFunc { | ||||
|   | ||||
							
								
								
									
										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") | ||||
| } | ||||
| @@ -17,171 +17,25 @@ | ||||
| package utils | ||||
|  | ||||
| import ( | ||||
| 	"errors" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"os/user" | ||||
| 	"strconv" | ||||
| 	"syscall" | ||||
|  | ||||
| 	"github.com/leonelquinteros/gotext" | ||||
| 	"github.com/urfave/cli/v2" | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" | ||||
| 	appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants" | ||||
| ) | ||||
|  | ||||
| func GetUidGidAlrUserString() (string, string, error) { | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| // IsNotRoot проверяет, что текущий пользователь не является root | ||||
| func IsNotRoot() bool { | ||||
| 	return os.Getuid() != 0 | ||||
| } | ||||
|  | ||||
| func EnsureIsAlrUser() error { | ||||
| 	uid, gid, err := GetUidGidAlrUser() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	newUid := syscall.Getuid() | ||||
| 	if newUid != uid { | ||||
| 		return errors.New("uid don't matches requested") | ||||
| 	} | ||||
| 	newGid := syscall.Getgid() | ||||
| 	if newGid != gid { | ||||
| 		return errors.New("gid don't matches requested") | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // EnuseIsPrivilegedGroupMember проверяет, что пользователь является членом привилегированной группы (wheel/sudo) | ||||
| // DEPRECATED: используйте CheckUserPrivileges() из utils.go | ||||
| func EnuseIsPrivilegedGroupMember() error { | ||||
| 	currentUser, err := user.Current() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	group, err := user.LookupGroup(constants.PrivilegedGroup) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	groups, err := currentUser.GroupIds() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for _, gid := range groups { | ||||
| 		if gid == group.Gid { | ||||
| 			return nil | ||||
| 		} | ||||
| 	} | ||||
| 	return cliutils.FormatCliExit(gotext.Get("You need to be a %s member to perform this action", constants.PrivilegedGroup), nil) | ||||
| } | ||||
|  | ||||
| func EscalateToRootGid() error { | ||||
| 	return syscall.Setgid(0) | ||||
| } | ||||
|  | ||||
| func EscalateToRootUid() error { | ||||
| 	return syscall.Setuid(0) | ||||
| } | ||||
|  | ||||
| func EscalateToRoot() error { | ||||
| 	err := EscalateToRootUid() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	err = EscalateToRootGid() | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	return nil | ||||
| 	return CheckUserPrivileges() | ||||
| } | ||||
|  | ||||
| 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,149 @@ | ||||
|  | ||||
| package utils | ||||
|  | ||||
| import "golang.org/x/sys/unix" | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"os/user" | ||||
| 	"strings" | ||||
|  | ||||
| 	"golang.org/x/sys/unix" | ||||
| ) | ||||
|  | ||||
| func NoNewPrivs() error { | ||||
| 	return unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) | ||||
| } | ||||
|  | ||||
| // EnsureTempDirWithRootOwner создает каталог в /tmp/alr или /var/cache/alr с правами для привилегированной группы | ||||
| // Все каталоги в /tmp/alr и /var/cache/alr принадлежат root:привилегированная_группа с правами 2775 | ||||
| // Для других каталогов использует стандартные права | ||||
| func EnsureTempDirWithRootOwner(path string, mode os.FileMode) error { | ||||
| 	needsElevation := strings.HasPrefix(path, "/tmp/alr") || strings.HasPrefix(path, "/var/cache/alr") | ||||
|  | ||||
| 	if needsElevation { | ||||
| 		// В CI или если мы уже root, не нужно использовать sudo | ||||
| 		isRoot := os.Geteuid() == 0 | ||||
| 		isCI := os.Getenv("CI") == "true" | ||||
|  | ||||
| 		// В CI создаем директории с обычными правами | ||||
| 		if isCI { | ||||
| 			err := os.MkdirAll(path, mode) | ||||
| 			if err != nil { | ||||
| 				return err | ||||
| 			} | ||||
| 			// В CI не используем группу wheel и не меняем права | ||||
| 			// Устанавливаем базовые права 777 для временных каталогов | ||||
| 			chmodCmd := exec.Command("chmod", "777", path) | ||||
| 			chmodCmd.Run() // Игнорируем ошибки | ||||
| 			return nil | ||||
| 		} | ||||
|  | ||||
| 		// Для обычной работы устанавливаем права и привилегированную группу | ||||
| 		permissions := "2775" | ||||
| 		group := GetPrivilegedGroup() | ||||
|  | ||||
| 		var mkdirCmd, chmodCmd, chownCmd *exec.Cmd | ||||
| 		if isRoot { | ||||
| 			// Выполняем команды напрямую без sudo | ||||
| 			mkdirCmd = exec.Command("mkdir", "-p", path) | ||||
| 			chmodCmd = exec.Command("chmod", permissions, path) | ||||
| 			chownCmd = exec.Command("chown", "root:"+group, path) | ||||
| 		} else { | ||||
| 			// Используем sudo для всех операций с привилегированными каталогами | ||||
| 			mkdirCmd = exec.Command("sudo", "mkdir", "-p", path) | ||||
| 			chmodCmd = exec.Command("sudo", "chmod", permissions, path) | ||||
| 			chownCmd = exec.Command("sudo", "chown", "root:"+group, path) | ||||
| 		} | ||||
|  | ||||
| 		// Создаем директорию через sudo если нужно | ||||
| 		err := mkdirCmd.Run() | ||||
| 		if err != nil { | ||||
| 			// Игнорируем ошибку если директория уже существует | ||||
| 			if !isRoot { | ||||
| 				// Проверяем существует ли директория | ||||
| 				if _, statErr := os.Stat(path); statErr != nil { | ||||
| 					return fmt.Errorf("не удалось создать директорию %s: %w", path, err) | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Устанавливаем права с setgid битом для наследования группы | ||||
| 		err = chmodCmd.Run() | ||||
| 		if err != nil { | ||||
| 			if !isRoot { | ||||
| 				return fmt.Errorf("не удалось установить права на %s: %w", path, err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		// Устанавливаем владельца root:группа | ||||
| 		err = chownCmd.Run() | ||||
| 		if err != nil { | ||||
| 			if !isRoot { | ||||
| 				return fmt.Errorf("не удалось установить владельца на %s: %w", path, err) | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// Для остальных каталогов обычное создание | ||||
| 	return os.MkdirAll(path, mode) | ||||
| } | ||||
|  | ||||
| // IsUserInGroup проверяет, состоит ли пользователь в указанной группе | ||||
| func IsUserInGroup(username, groupname string) bool { | ||||
| 	u, err := user.Lookup(username) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	groups, err := u.GroupIds() | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	targetGroup, err := user.LookupGroup(groupname) | ||||
| 	if err != nil { | ||||
| 		return false | ||||
| 	} | ||||
|  | ||||
| 	for _, gid := range groups { | ||||
| 		if gid == targetGroup.Gid { | ||||
| 			return true | ||||
| 		} | ||||
| 	} | ||||
| 	return false | ||||
| } | ||||
|  | ||||
| // CheckUserPrivileges проверяет, что пользователь имеет необходимые привилегии для работы с ALR | ||||
| // Пользователь должен быть root или состоять в группе wheel/sudo | ||||
| func CheckUserPrivileges() error { | ||||
| 	// Если пользователь root - все в порядке | ||||
| 	if os.Geteuid() == 0 { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	// В CI не проверяем привилегии | ||||
| 	if os.Getenv("CI") == "true" { | ||||
| 		return nil | ||||
| 	} | ||||
|  | ||||
| 	currentUser, err := user.Current() | ||||
| 	if err != nil { | ||||
| 		return fmt.Errorf("не удалось получить информацию о текущем пользователе: %w", err) | ||||
| 	} | ||||
|  | ||||
| 	privilegedGroup := GetPrivilegedGroup() | ||||
|  | ||||
| 	// Проверяем членство в привилегированной группе | ||||
| 	if !IsUserInGroup(currentUser.Username, privilegedGroup) { | ||||
| 		return fmt.Errorf("пользователь %s не имеет необходимых привилегий для работы с ALR.\n"+ | ||||
| 			"Для работы с ALR необходимо быть пользователем root или состоять в группе %s.\n"+ | ||||
| 			"Выполните команду: sudo usermod -a -G %s %s\n"+ | ||||
| 			"Затем перезайдите в систему или выполните: newgrp %s", | ||||
| 			currentUser.Username, privilegedGroup, privilegedGroup, currentUser.Username, privilegedGroup) | ||||
| 	} | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|   | ||||
							
								
								
									
										4
									
								
								list.go
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								list.go
									
									
									
									
									
								
							| @@ -35,7 +35,6 @@ import ( | ||||
| 	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/overrides" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" | ||||
| ) | ||||
|  | ||||
| @@ -60,9 +59,6 @@ func ListCmd() *cli.Command { | ||||
| 			}, | ||||
| 		}, | ||||
| 		Action: func(c *cli.Context) error { | ||||
| 			if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			ctx := c.Context | ||||
|  | ||||
|   | ||||
							
								
								
									
										1
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								main.go
									
									
									
									
									
								
							| @@ -87,7 +87,6 @@ func GetApp() *cli.App { | ||||
| 			// Internal commands | ||||
| 			InternalBuildCmd(), | ||||
| 			InternalInstallCmd(), | ||||
| 			InternalMountCmd(), | ||||
| 			InternalReposCmd(), | ||||
| 		}, | ||||
| 		Before: func(c *cli.Context) error { | ||||
|   | ||||
							
								
								
									
										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() | ||||
|  | ||||
| 		if slices.Contains(names, name) { | ||||
| 			err = os.Link(filepath.Join(cacheDir, name), dest) | ||||
| 			err = linkOrCopy(filepath.Join(cacheDir, name), dest) | ||||
| 			if err != nil { | ||||
| 				return false, err | ||||
| 			} | ||||
| 			return true, nil | ||||
| 		} | ||||
| 	case TypeDir: | ||||
| 		err := linkDir(cacheDir, dest) | ||||
| 		err := linkOrCopyDir(cacheDir, dest) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
| @@ -296,8 +296,40 @@ func handleCache(cacheDir, dest, name string, t Type) (bool, error) { | ||||
| 	return false, nil | ||||
| } | ||||
|  | ||||
| // Функция linkDir рекурсивно создает жесткие ссылки для файлов из каталога src в каталог dest | ||||
| func linkDir(src, dest string) error { | ||||
| // linkOrCopy пытается создать жесткую ссылку, а если не получается - копирует файл | ||||
| 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 { | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| @@ -317,7 +349,7 @@ func linkDir(src, dest string) error { | ||||
| 			return os.MkdirAll(newPath, info.Mode()) | ||||
| 		} | ||||
|  | ||||
| 		return os.Link(path, newPath) | ||||
| 		return linkOrCopy(path, newPath) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -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 { | ||||
| 		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/ и /var/cache/alr/ | ||||
| 	// Проверяем с слешем в конце, чтобы исключить тестовые директории вроде /tmp/alr-test-XXX | ||||
| 	if strings.HasPrefix(itemPath, "/tmp/alr/") || strings.HasPrefix(itemPath, "/var/cache/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 { | ||||
| 	t.Helper() | ||||
|  | ||||
| 	dir, err := os.MkdirTemp("/tmp", "alr-dlcache-test.*") | ||||
| 	dir, err := os.MkdirTemp("", "alr-dlcache-test.*") | ||||
| 	if err != nil { | ||||
| 		panic(err) | ||||
| 	} | ||||
| @@ -57,7 +57,7 @@ func prepare(t *testing.T) *TestALRConfig { | ||||
|  | ||||
| func cleanup(t *testing.T, cfg *TestALRConfig) { | ||||
| 	t.Helper() | ||||
| 	os.Remove(cfg.CacheDir) | ||||
| 	os.RemoveAll(cfg.CacheDir) | ||||
| } | ||||
|  | ||||
| func TestNew(t *testing.T) { | ||||
| @@ -82,6 +82,12 @@ func TestNew(t *testing.T) { | ||||
| 	fi, err := os.Stat(dir) | ||||
| 	if err != nil { | ||||
| 		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() { | ||||
|   | ||||
							
								
								
									
										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) | ||||
| } | ||||
| @@ -28,6 +28,7 @@ type Config struct { | ||||
| 	Repos               []Repo   `json:"repo" koanf:"repo"` | ||||
| 	AutoPull            bool     `json:"autoPull" koanf:"autoPull"` | ||||
| 	LogLevel            string   `json:"logLevel" koanf:"logLevel"` | ||||
| 	UpdateSystemOnUpgrade bool   `json:"updateSystemOnUpgrade" koanf:"updateSystemOnUpgrade"` | ||||
| } | ||||
|  | ||||
| // Repo represents a ALR repo within a configuration file | ||||
|   | ||||
| @@ -30,7 +30,7 @@ func RefreshCmd() *cli.Command { | ||||
| 		Usage:   gotext.Get("Pull all repositories that have changed"), | ||||
| 		Aliases: []string{"ref"}, | ||||
| 		Action: func(c *cli.Context) error { | ||||
| 			if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { | ||||
| 			if err := utils.CheckUserPrivileges(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
|   | ||||
							
								
								
									
										3
									
								
								repo.go
									
									
									
									
									
								
							
							
						
						
									
										3
									
								
								repo.go
									
									
									
									
									
								
							| @@ -114,9 +114,6 @@ func RemoveRepoCmd() *cli.Command { | ||||
| 				return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) | ||||
| 			} | ||||
|  | ||||
| 			if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			deps, err = appbuilder. | ||||
| 				New(ctx). | ||||
|   | ||||
							
								
								
									
										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,6 +32,13 @@ error() { | ||||
|  | ||||
| installPkg() { | ||||
|   rootCmd="" | ||||
|    | ||||
|   # Проверяем, запущен ли скрипт от root | ||||
|   if [ "$(id -u)" = "0" ]; then | ||||
|     # Если root, не используем sudo/doas | ||||
|     rootCmd="" | ||||
|   else | ||||
|     # Если не root, ищем команду повышения привилегий | ||||
|     if command -v doas &>/dev/null; then | ||||
|       rootCmd="doas" | ||||
|     elif command -v sudo &>/dev/null; then | ||||
| @@ -39,6 +46,7 @@ installPkg() { | ||||
|     else | ||||
|       warn "Не обнаружена команда повышения привилегий (например, sudo, doas)" | ||||
|     fi | ||||
|   fi | ||||
|  | ||||
|   case $1 in | ||||
|   pacman) $rootCmd pacman --noconfirm -U "${@:2}" ;; | ||||
| @@ -48,10 +56,46 @@ installPkg() { | ||||
|   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 | ||||
|   error "Этот скрипт требует команду curl. Пожалуйста, установите её и запустите снова." | ||||
| 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="" | ||||
| pkgMgr="" | ||||
| if command -v pacman &>/dev/null; then | ||||
| @@ -88,25 +132,50 @@ else | ||||
| fi | ||||
|  | ||||
| if [ -z "$noPkgMgr" ]; then | ||||
|   info "Получение списка файлов с https://gitea.plemya-x.ru/Plemya-x/ALR/releases" | ||||
|   info "Получение списка релизов через API Gitea" | ||||
|  | ||||
|   # Изменено URL и регулярное выражение для списка файлов | ||||
|   # Используем API для получения последнего релиза | ||||
|   releases=$(curl -s "https://gitea.plemya-x.ru/api/v1/repos/Plemya-x/ALR/releases") | ||||
|  | ||||
|   if [ -z "$releases" ] || [ "$releases" = "null" ]; then | ||||
|     error "Не удалось получить список релизов. Проверьте соединение с интернетом." | ||||
|   fi | ||||
|  | ||||
|   # Получаем URL последнего релиза | ||||
|   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 | ||||
|  | ||||
|   # Извлечение списка файлов из HTML | ||||
|   fileList=$(echo "$pageContent" | grep -oP '(?<=href=").*?(?=")' | grep -E 'alr-bin.*\.(pkg.tar.zst|rpm|deb)') | ||||
|   if [ -z "$fileList" ]; then | ||||
|     warn "Не найдены готовые пакеты в последнем релизе" | ||||
|     warn "Возможно, для вашего дистрибутива нужно собрать пакет из исходников" | ||||
|     warn "Инструкции по сборке: https://gitea.plemya-x.ru/Plemya-x/ALR" | ||||
|     error "Не удалось получить список пакетов для загрузки" | ||||
|   fi | ||||
|  | ||||
|   echo "Полученный список файлов:" | ||||
|   echo "$fileList" | ||||
|   info "Получен список файлов релиза" | ||||
|  | ||||
|   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 | ||||
|       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 | ||||
|       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 | ||||
|       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 | ||||
|       error "Не поддерживаемый менеджер пакетов для автоматической установки" | ||||
|   fi | ||||
| @@ -119,18 +188,35 @@ if [ -z "$noPkgMgr" ]; then | ||||
|  | ||||
|   fname="$(mktemp -u -p /tmp "alr.XXXXXXXXXX").${pkgFormat}" | ||||
|  | ||||
|   info "Загрузка пакета ALR" | ||||
|   curl -o $fname -L "$latestFile" | ||||
|   # Настраиваем trap для очистки временного файла | ||||
|   trap "rm -f $fname" EXIT | ||||
|  | ||||
|   if [ ! -f "$fname" ]; then | ||||
|       error "Ошибка загрузки пакета ALR" | ||||
|   info "Загрузка пакета ALR" | ||||
|   info "URL: $latestFile" | ||||
|  | ||||
|   # Загружаем с проверкой кода возврата | ||||
|   if ! curl -f -L -o "$fname" "$latestFile"; then | ||||
|       error "Ошибка загрузки пакета ALR. Проверьте подключение к интернету." | ||||
|   fi | ||||
|  | ||||
|   # Проверяем что файл не пустой | ||||
|   if [ ! -s "$fname" ]; then | ||||
|       error "Загруженный файл пустой или поврежден" | ||||
|   fi | ||||
|  | ||||
|   # Показываем размер загруженного файла | ||||
|   fileSize=$(du -h "$fname" | cut -f1) | ||||
|   info "Загружен пакет размером $fileSize" | ||||
|  | ||||
|   info "Установка пакета ALR" | ||||
|   installPkg "$pkgMgr" "$fname" | ||||
|  | ||||
|   # Отправляем статистику установки | ||||
|   trackInstallation | ||||
|  | ||||
|   info "Очистка" | ||||
|   rm "$fname" | ||||
|   rm -f "$fname" | ||||
|   trap - EXIT | ||||
|  | ||||
|   info "Готово!" | ||||
| 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" | ||||
| 	"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/utils" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" | ||||
| ) | ||||
| @@ -72,9 +71,6 @@ func SearchCmd() *cli.Command { | ||||
| 			}, | ||||
| 		}, | ||||
| 		Action: func(c *cli.Context) error { | ||||
| 			if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			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 { | ||||
| 			if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			installer, installerClose, err := build.GetSafeInstaller() | ||||
| 			if err != nil { | ||||
| @@ -65,9 +62,6 @@ func UpgradeCmd() *cli.Command { | ||||
| 			} | ||||
| 			defer installerClose() | ||||
|  | ||||
| 			if err := utils.ExitIfCantSetNoNewPrivs(); err != nil { | ||||
| 				return err | ||||
| 			} | ||||
|  | ||||
| 			scripter, scripterClose, err := build.GetSafeScriptExecutor() | ||||
| 			if err != nil { | ||||
| @@ -90,6 +84,19 @@ func UpgradeCmd() *cli.Command { | ||||
| 			} | ||||
| 			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( | ||||
| 				deps.Cfg, | ||||
| 				deps.Manager, | ||||
|   | ||||
		Reference in New Issue
	
	Block a user