24 Commits

Author SHA1 Message Date
8bc82cb95c Добавление статистики
Исправление работы с мультипакетами
2025-09-01 01:32:43 +03:00
9783ce37de Добавление возможности обновления системным пакетным менеджером при alr up 2025-08-28 12:03:14 +03:00
b852688ab0 Исправление фильтрации имён пакетов в скрипте установки 2025-08-27 12:49:03 +03:00
2ff5e6f7b6 изменение способа определения имён для добавления в релиз №2 2025-08-27 12:33:05 +03:00
c9639b7073 изменение способа определения имён для добавления в релиз 2025-08-27 12:22:22 +03:00
c1847e1191 изменение способа определения имён для добавления в релиз 2025-08-27 12:10:42 +03:00
f2b0f57c12 1. internal/manager/common.go - Модифицировали getCmd для
проверки root/CI перед использованием sudo
  2. internal/utils/utils.go - Функция
  EnsureTempDirWithRootOwner теперь не использует группу
  wheel в CI
  3. internal/utils/cmd.go - Функция
  EnuseIsPrivilegedGroupMember пропускает проверку wheel в
  CI
  4. fix.go - Добавили функцию execWithPrivileges для
  условного использования sudo
  5. scripts/install.sh - Добавили проверку root перед
  использованием sudo
2025-08-27 11:46:18 +03:00
59cc41e94c Внесение логики для запуска из под root 2025-08-27 01:45:54 +03:00
75ece6dfcc Исправление для авторелизов 2025-08-27 01:06:58 +03:00
6af712f1d5 исправление pre-commit hooks для корректной работы с изменёнными файлами 2025-08-27 00:49:16 +03:00
bad225c6b1 - Добавлено автоматическое определение архитектуры системы
- Использование API Gitea вместо парсинга HTML
- Добавлен fallback на парсинг HTML если API недоступен
- Улучшена обработка ошибок при загрузке
- Добавлена проверка целостности загруженного файла
- Использование trap для гарантированной очистки временных файлов
- Исправлена логика выбора файлов для разных архитектур
- Добавлен вывод размера загруженного пакета"
2025-08-27 00:36:58 +03:00
4b3bf44aaa fix: улучшение pre-commit hooks для правильной обработки изменений файлов
- Создан fmt-precommit.sh для корректной обработки форматирования
- Создан test-coverage-precommit.sh для обработки изменений покрытия
- Скрипты всегда возвращают 0 при успешном выполнении
- Автоматически добавляют изменённые файлы в staging area
2025-08-27 00:14:24 +03:00
67b3c40430 исправление README.md 2025-08-27 00:06:24 +03:00
4948e6b8fc исправление fmt 2025-08-26 23:49:36 +03:00
292125a8ff исправление теста dlcache_test.go №2 2025-08-26 23:41:33 +03:00
77055aa2cb исправление теста dlcache_test.go 2025-08-26 23:16:31 +03:00
737bf68f95 исправление i18n-precommit 2025-08-26 22:43:39 +03:00
1089e8a3f3 Исправление работоспособности pre-commit.yaml 2 2025-08-26 22:25:03 +03:00
aa42ab0607 Исправление работоспособности pre-commit.yaml 2025-08-26 22:14:01 +03:00
51fa7ca6fb убрана лишняя зависимость bindfs и избыточное использование дополнительного пользователя alr 2025-08-26 22:09:28 +03:00
ab41700004 первичная итерация генератора из aur пакетов 2025-08-21 18:47:37 +03:00
7cb1bc9548 первичная итерация генератора из aur пакетов 2025-08-21 18:44:43 +03:00
07187da423 - Изменение ссылки на wiki в README.md 2025-07-27 23:50:45 +03:00
802fe2b0b2 tag 0.0.26 2025-07-11 14:53:52 +03:00
46 changed files with 1792 additions and 561 deletions

View File

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

View File

@@ -47,7 +47,7 @@ jobs:
- name: Prepare for install - name: Prepare for install
run: | run: |
apt-get update && apt-get install -y libcap2-bin bindfs apt-get update
- name: Build alr - name: Build alr
env: env:
@@ -84,37 +84,32 @@ jobs:
sed -i "s/version='[0-9]\+\.[0-9]\+\.[0-9]\+'/version='${{ env.VERSION }}'/g" alr-default/alr-bin/alr.sh sed -i "s/version='[0-9]\+\.[0-9]\+\.[0-9]\+'/version='${{ env.VERSION }}'/g" alr-default/alr-bin/alr.sh
sed -i "s/release='[0-9]\+'/release='1'/g" alr-default/alr-bin/alr.sh sed -i "s/release='[0-9]\+'/release='1'/g" alr-default/alr-bin/alr.sh
# - name: Install alr - name: Install alr
# run: | env:
# make install CREATE_SYSTEM_RESOURCES: 0
#
# # temporary fix
# groupadd wheel
# usermod -aG wheel root
# - 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
run: | run: |
cd alr-default make install
git config user.name "gitea"
git config user.email "admin@plemya-x.ru" - name: Prepare directories for ALR
git add . run: |
git commit -m "Обновление версии до ${{ env.VERSION }}" # Создаём необходимые директории для работы alr build
git push 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

View File

@@ -1,69 +0,0 @@
# ALR - Any Linux Repository
# Copyright (C) 2025 The ALR Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
name: Update alr-git
on:
push:
branches:
- master
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- name: Install the latest version of uv
uses: astral-sh/setup-uv@v6
- name: Setup alr-spec
run: |
uv tool install alr-spec==0.0.5
- name: Install alr
run: |
apt-get update && apt-get install -y libcap2-bin
curl -fsS https://gitea.plemya-x.ru/Plemya-x/ALR/raw/branch/master/scripts/install.sh | bash
- name: Checkout this repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set ALR version
run: |
echo "NEW_ALR_VERSION=$(alr helper git-version)" >> $GITHUB_ENV
- name: Checkout alr-default repository
uses: actions/checkout@v4
with:
repository: Plemya-x/alr-default
token: ${{ secrets.GITEAPUBLIC }}
path: alr-default
- name: Update version
working-directory: ./alr-default/alr-git
run: |
alr-spec set-field version $NEW_ALR_VERSION
alr-spec set-field release 1
- name: Commit changes
run: |
cd alr-default
git config user.name "gitea"
git config user.email "admin@plemya-x.ru"
git add .
git commit -m "Обновление версии до $NEW_ALR_VERSION"
git push

7
.gitignore vendored
View File

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

View File

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

View File

@@ -49,17 +49,12 @@ install: \
$(INSTALLED_BIN): $(BIN) $(INSTALLED_BIN): $(BIN)
install -Dm755 $< $@ install -Dm755 $< $@
ifeq ($(CREATE_SYSTEM_RESOURCES),1) 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 \ @for dir in $(ROOT_DIRS); do \
install -d -o alr -g alr -m 755 $$dir; \ install -d -m 775 $$dir; \
chgrp wheel $$dir; \
done done
else else
@echo "Skipping user and root dir creation (CREATE_SYSTEM_RESOURCES=0)" @echo "Skipping root dir creation (CREATE_SYSTEM_RESOURCES=0)"
endif endif
$(INSTALLED_BASH_COMPLETION): $(BASH_COMPLETION) $(INSTALLED_BASH_COMPLETION): $(BASH_COMPLETION)
@@ -93,7 +88,7 @@ i18n:
bash scripts/i18n-badge.sh bash scripts/i18n-badge.sh
test-coverage: test-coverage:
go test ./... -v -coverpkg=./... -coverprofile=coverage.out go test -tags=test ./... -v -coverpkg=./... -coverprofile=coverage.out
bash scripts/coverage-badge.sh bash scripts/coverage-badge.sh
update-deps-cve: update-deps-cve:

View File

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

View File

@@ -72,12 +72,6 @@ func BuildCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Error getting working directory"), err) return cliutils.FormatCliExit(gotext.Get("Error getting working directory"), err)
} }
wd, wdCleanup, err := Mount(wd)
if err != nil {
return err
}
defer wdCleanup()
ctx := c.Context ctx := c.Context
deps, err := appbuilder. deps, err := appbuilder.
@@ -156,19 +150,9 @@ func BuildCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Nothing to build"), nil) return cliutils.FormatCliExit(gotext.Get("Nothing to build"), nil)
} }
if scriptArgs != nil {
scriptFile := filepath.Base(scriptArgs.Script)
newScriptDir, scriptDirCleanup, err := Mount(filepath.Dir(scriptArgs.Script))
if err != nil {
return err
}
defer scriptDirCleanup()
scriptArgs.Script = filepath.Join(newScriptDir, scriptFile)
}
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
installer, installerClose, err := build.GetSafeInstaller() installer, installerClose, err := build.GetSafeInstaller()
if err != nil { if err != nil {
@@ -176,9 +160,7 @@ func BuildCmd() *cli.Command {
} }
defer installerClose() defer installerClose()
if err := utils.ExitIfCantSetNoNewPrivs(); err != nil {
return err
}
scripter, scripterClose, err := build.GetSafeScriptExecutor() scripter, scripterClose, err := build.GetSafeScriptExecutor()
if err != nil { if err != nil {

View File

@@ -76,6 +76,7 @@ var configKeys = []string{
"autoPull", "autoPull",
"logLevel", "logLevel",
"ignorePkgUpdates", "ignorePkgUpdates",
"updateSystemOnUpgrade",
} }
func SetConfig() *cli.Command { func SetConfig() *cli.Command {
@@ -137,6 +138,12 @@ func SetConfig() *cli.Command {
} }
} }
deps.Cfg.System.SetIgnorePkgUpdates(updates) deps.Cfg.System.SetIgnorePkgUpdates(updates)
case "updateSystemOnUpgrade":
boolValue, err := strconv.ParseBool(value)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("invalid boolean value for %s: %s", key, value), err)
}
deps.Cfg.System.SetUpdateSystemOnUpgrade(boolValue)
case "repo", "repos": case "repo", "repos":
return cliutils.FormatCliExit(gotext.Get("use 'repo add/remove' commands to manage repositories"), nil) return cliutils.FormatCliExit(gotext.Get("use 'repo add/remove' commands to manage repositories"), nil)
default: default:
@@ -206,6 +213,8 @@ func GetConfig() *cli.Command {
} else { } else {
fmt.Println(strings.Join(updates, ", ")) fmt.Println(strings.Join(updates, ", "))
} }
case "updateSystemOnUpgrade":
fmt.Println(deps.Cfg.UpdateSystemOnUpgrade())
case "repo", "repos": case "repo", "repos":
repos := deps.Cfg.Repos() repos := deps.Cfg.Repos()
if len(repos) == 0 { if len(repos) == 0 {

146
fix.go
View File

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

23
gen.go
View File

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

View File

@@ -31,7 +31,6 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" "gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
) )
@@ -48,9 +47,6 @@ func InfoCmd() *cli.Command {
}, },
}, },
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error { BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
ctx := c.Context ctx := c.Context
deps, err := appbuilder. deps, err := appbuilder.
@@ -74,9 +70,7 @@ func InfoCmd() *cli.Command {
return nil return nil
}), }),
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { // Запуск от текущего пользователя
return err
}
args := c.Args() args := c.Args()
if args.Len() < 1 { if args.Len() < 1 {

View File

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

View File

@@ -17,14 +17,8 @@
package main package main
import ( import (
"bufio"
"errors"
"fmt"
"log/slog" "log/slog"
"os" "os"
"os/exec"
"os/user"
"path/filepath"
"syscall" "syscall"
"github.com/hashicorp/go-hclog" "github.com/hashicorp/go-hclog"
@@ -36,7 +30,6 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger" "gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
@@ -52,9 +45,6 @@ func InternalBuildCmd() *cli.Command {
slog.Debug("start _internal-safe-script-executor", "uid", syscall.Getuid(), "gid", syscall.Getgid()) slog.Debug("start _internal-safe-script-executor", "uid", syscall.Getuid(), "gid", syscall.Getgid())
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
cfg := config.New() cfg := config.New()
err := cfg.Load() err := cfg.Load()
@@ -92,9 +82,6 @@ func InternalReposCmd() *cli.Command {
Action: utils.RootNeededAction(func(ctx *cli.Context) error { Action: utils.RootNeededAction(func(ctx *cli.Context) error {
logger.SetupForGoPlugin() logger.SetupForGoPlugin()
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
deps, err := appbuilder. deps, err := appbuilder.
New(ctx.Context). New(ctx.Context).
@@ -129,16 +116,7 @@ func InternalInstallCmd() *cli.Command {
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
logger.SetupForGoPlugin() logger.SetupForGoPlugin()
if err := utils.EnsureIsAlrUser(); err != nil { // Запуск от текущего пользователя, повышение прав будет через sudo при необходимости
return err
}
// Before escalating the rights, we made sure that
// this is an ALR user, so it looks safe.
err := utils.EscalateToRootUid()
if err != nil {
return cliutils.FormatCliExit("cannot escalate to root", err)
}
deps, err := appbuilder. deps, err := appbuilder.
New(c.Context). New(c.Context).
@@ -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
},
}
}

View File

@@ -32,6 +32,7 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config" "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/stats"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
@@ -401,9 +402,19 @@ func (b *Builder) BuildPackage(
// We filter so as not to re-build what has already been built at the `installBuildDeps` stage. // We filter so as not to re-build what has already been built at the `installBuildDeps` stage.
var filteredDepends []string var filteredDepends []string
// Создаем набор подпакетов текущего мультипакета для исключения циклических зависимостей
currentPackageNames := make(map[string]struct{})
for _, pkg := range input.packages {
currentPackageNames[pkg] = struct{}{}
}
for _, d := range depends { for _, d := range depends {
if _, found := depNames[d]; !found { if _, found := depNames[d]; !found {
filteredDepends = append(filteredDepends, d) // Исключаем зависимости, которые являются подпакетами текущего мультипакета
if _, isCurrentPackage := currentPackageNames[d]; !isCurrentPackage {
filteredDepends = append(filteredDepends, d)
}
} }
} }
@@ -528,6 +539,13 @@ func (b *Builder) InstallALRPackages(
if err != nil { if err != nil {
return err return err
} }
// Отслеживание установки ALR пакетов
for _, dep := range res {
if stats.ShouldTrackPackage(dep.Name) {
stats.TrackInstallation(ctx, dep.Name, "upgrade")
}
}
} }
return nil return nil
@@ -552,11 +570,13 @@ func (b *Builder) BuildALRDeps(
repoDeps = notFound repoDeps = notFound
// Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез // Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез
pkgs := cliutils.FlattenPkgs( // Для зависимостей указываем isDependency = true
pkgs := cliutils.FlattenPkgsWithContext(
ctx, ctx,
found, found,
"install", "install",
input.BuildOpts().Interactive, input.BuildOpts().Interactive,
true,
) )
type item struct { type item struct {
pkg *alrsh.Package pkg *alrsh.Package
@@ -691,6 +711,13 @@ func (i *Builder) InstallPkgs(
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Отслеживание установки локальных пакетов
for _, dep := range builtDeps {
if stats.ShouldTrackPackage(dep.Name) {
stats.TrackInstallation(ctx, dep.Name, "install")
}
}
} }
if len(repoDeps) > 0 { if len(repoDeps) > 0 {
@@ -700,6 +727,13 @@ func (i *Builder) InstallPkgs(
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Отслеживание установки пакетов из репозитория
for _, pkg := range repoDeps {
if stats.ShouldTrackPackage(pkg) {
stats.TrackInstallation(ctx, pkg, "install")
}
}
} }
return builtDeps, nil return builtDeps, nil

View File

@@ -44,9 +44,9 @@ var HandshakeConfig = plugin.HandshakeConfig{
func setCommonCmdEnv(cmd *exec.Cmd) { func setCommonCmdEnv(cmd *exec.Cmd) {
cmd.Env = []string{ cmd.Env = []string{
"HOME=/var/cache/alr", "HOME=" + os.Getenv("HOME"),
"LOGNAME=alr", "LOGNAME=" + os.Getenv("USER"),
"USER=alr", "USER=" + os.Getenv("USER"),
"PATH=/usr/bin:/bin:/usr/local/bin", "PATH=/usr/bin:/bin:/usr/local/bin",
} }
for _, env := range os.Environ() { for _, env := range os.Environ() {
@@ -102,9 +102,7 @@ func getSafeExecutor[T any](subCommand, pluginName string) (T, func(), error) {
Cmd: cmd, Cmd: cmd,
Logger: logger.GetHCLoggerAdapter(), Logger: logger.GetHCLoggerAdapter(),
SkipHostEnv: true, SkipHostEnv: true,
UnixSocketConfig: &plugin.UnixSocketConfig{ UnixSocketConfig: &plugin.UnixSocketConfig{},
Group: "alr",
},
SyncStderr: os.Stderr, SyncStderr: os.Stderr,
}) })
rpcClient, err := client.Client() rpcClient, err := client.Client()

View File

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

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ package config
import ( import (
"fmt" "fmt"
"os"
"path/filepath" "path/filepath"
"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
@@ -55,7 +56,13 @@ func defaultConfigKoanf() *koanf.Koanf {
"ignorePkgUpdates": []string{}, "ignorePkgUpdates": []string{},
"logLevel": "info", "logLevel": "info",
"autoPull": true, "autoPull": true,
"repos": []types.Repo{}, "updateSystemOnUpgrade": false,
"repos": []types.Repo{
{
Name: "alr-default",
URL: "https://gitea.plemya-x.ru/Plemya-x/alr-default.git",
},
},
} }
if err := k.Load(confmap.Provider(defaults, "."), nil); err != nil { if err := k.Load(confmap.Provider(defaults, "."), nil); err != nil {
panic(k) panic(k)
@@ -98,8 +105,20 @@ func (c *ALRConfig) Load() error {
c.paths.UserConfigPath = constants.SystemConfigPath c.paths.UserConfigPath = constants.SystemConfigPath
c.paths.CacheDir = constants.SystemCachePath c.paths.CacheDir = constants.SystemCachePath
c.paths.RepoDir = filepath.Join(c.paths.CacheDir, "repo") c.paths.RepoDir = filepath.Join(c.paths.CacheDir, "repo")
c.paths.PkgsDir = filepath.Join(c.paths.CacheDir, "pkgs") c.paths.PkgsDir = filepath.Join(constants.TempDir, "pkgs") // Перемещаем в /tmp/alr/pkgs
c.paths.DBPath = filepath.Join(c.paths.CacheDir, "db") c.paths.DBPath = filepath.Join(c.paths.CacheDir, "alr.db")
// Проверяем существование кэш-директории, но не пытаемся создать
if _, err := os.Stat(c.paths.CacheDir); err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("failed to check cache directory: %w", err)
}
}
// Выполняем миграцию конфигурации при необходимости
if err := c.migrateConfig(); err != nil {
return fmt.Errorf("failed to migrate config: %w", err)
}
return nil return nil
} }
@@ -112,6 +131,45 @@ func (c *ALRConfig) ToYAML() (string, error) {
return string(data), nil return string(data), nil
} }
func (c *ALRConfig) migrateConfig() error {
// Проверяем, существует ли конфигурационный файл
if _, err := os.Stat(constants.SystemConfigPath); os.IsNotExist(err) {
// Если файла нет, но конфигурация уже загружена (из defaults или env),
// создаем файл с настройкой по умолчанию
needsCreation := false
// Проверяем, установлена ли переменная окружения ALR_UPDATESYSTEMONUPGRADE
if os.Getenv("ALR_UPDATESYSTEMONUPGRADE") == "" {
// Если переменная не установлена, проверяем наличие пакетов ALR
// чтобы определить, нужно ли включить эту опцию для обновления
needsCreation = true
}
if needsCreation {
// Устанавливаем значение false по умолчанию для новой опции
c.System.SetUpdateSystemOnUpgrade(false)
// Сохраняем конфигурацию
if err := c.System.Save(); err != nil {
// Если не удается сохранить - это не критично, продолжаем работу
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) RootCmd() string { return c.cfg.RootCmd } func (c *ALRConfig) RootCmd() string { return c.cfg.RootCmd }
func (c *ALRConfig) PagerStyle() string { return c.cfg.PagerStyle } func (c *ALRConfig) PagerStyle() string { return c.cfg.PagerStyle }
func (c *ALRConfig) AutoPull() bool { return c.cfg.AutoPull } func (c *ALRConfig) AutoPull() bool { return c.cfg.AutoPull }
@@ -120,4 +178,5 @@ func (c *ALRConfig) SetRepos(repos []types.Repo) { c.System.SetRepos(repos) }
func (c *ALRConfig) IgnorePkgUpdates() []string { return c.cfg.IgnorePkgUpdates } func (c *ALRConfig) IgnorePkgUpdates() []string { return c.cfg.IgnorePkgUpdates }
func (c *ALRConfig) LogLevel() string { return c.cfg.LogLevel } func (c *ALRConfig) LogLevel() string { return c.cfg.LogLevel }
func (c *ALRConfig) UseRootCmd() bool { return c.cfg.UseRootCmd } func (c *ALRConfig) UseRootCmd() bool { return c.cfg.UseRootCmd }
func (c *ALRConfig) UpdateSystemOnUpgrade() bool { return c.cfg.UpdateSystemOnUpgrade }
func (c *ALRConfig) GetPaths() *Paths { return c.paths } func (c *ALRConfig) GetPaths() *Paths { return c.paths }

View File

@@ -142,3 +142,10 @@ func (c *SystemConfig) SetRepos(v []types.Repo) {
panic(err) panic(err)
} }
} }
func (c *SystemConfig) SetUpdateSystemOnUpgrade(v bool) {
err := c.k.Set("updateSystemOnUpgrade", v)
if err != nil {
panic(err)
}
}

View File

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

View File

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

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

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

View File

@@ -0,0 +1,133 @@
# This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
# It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
#
# ALR - Any Linux Repository
# Copyright (C) 2025 The ALR Authors
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
# Generated from AUR package: {{.Name}}
# Package type: {{.PackageType}}
# AUR votes: {{.NumVotes}} | Popularity: {{printf "%.2f" .Popularity}}
# Original maintainer: {{.Maintainer}}
# Adapted for ALR by automation
name='{{.Name}}'
version='{{.Version}}'
release='1'
desc='{{.Description}}'
{{if ne .Description ""}}desc_ru='{{.Description}}'{{end}}
homepage='{{.URL}}'
maintainer="Евгений Храмов <xpamych@yandex.ru> (imported from AUR)"
{{if ne .Description ""}}maintainer_ru="Евгений Храмов <xpamych@yandex.ru> (импортирован из AUR)"{{end}}
architectures=({{.ArchitecturesString}})
license=({{.LicenseString}})
{{if .Provides}}provides=({{range .Provides}}'{{.}}' {{end}}){{end}}
{{if .Conflicts}}conflicts=({{range .Conflicts}}'{{.}}' {{end}}){{end}}
{{if .Replaces}}replaces=({{range .Replaces}}'{{.}}' {{end}}){{end}}
# Базовые зависимости
{{if .DependsString}}deps=({{.DependsString}}){{else}}deps=(){{end}}
{{if .MakeDependsString}}build_deps=({{.MakeDependsString}}){{else}}build_deps=(){{end}}
# Зависимости для конкретных дистрибутивов (адаптируйте под нужды пакета)
{{if .DependsString}}deps_arch=({{.DependsString}})
deps_debian=({{.DependsString}})
deps_altlinux=({{.DependsString}})
deps_alpine=({{.DependsString}}){{end}}
{{if and .MakeDependsString (ne .PackageType "bin")}}# Зависимости сборки для конкретных дистрибутивов
build_deps_arch=({{.MakeDependsString}})
build_deps_debian=({{.MakeDependsString}})
build_deps_altlinux=({{.MakeDependsString}})
build_deps_alpine=({{.MakeDependsString}}){{end}}
{{if .OptDependsString}}# Опциональные зависимости
opt_deps=(
{{.OptDependsString}}
){{end}}
# Источники из PKGBUILD
sources=({{range .Sources}}"{{.}}" {{end}})
checksums=({{range .Checksums}}'{{.}}' {{end}})
{{if .HasVersion}}# Функция версии для Git-пакетов
version() {
cd "$srcdir/{{.Name}}"
git-version
}
{{end}}
{{if .ScriptsString}}# Дополнительные скрипты
scripts=(
{{.ScriptsString}}
){{end}}
{{if or .PrepareFunc .HasPatches}}prepare() {
cd "$srcdir"{{if .PrepareFunc}}
# Из PKGBUILD:
{{.PrepareFunc}}{{else}}
# Применение патчей и подготовка исходников
# Раскомментируйте и адаптируйте при необходимости:
# patch -p1 < "${scriptdir}/fix.patch"{{end}}
}{{else}}# prepare() {
# cd "$srcdir"
# # Применение патчей и подготовка исходников при необходимости
# # patch -p1 < "${scriptdir}/fix.patch"
# }{{end}}
{{if ne .PackageType "bin"}}build() {
cd "$srcdir"{{if .BuildFunc}}
# Из PKGBUILD:
{{.BuildFunc}}{{else}}
# TODO: Адаптируйте команды сборки под конкретный проект ({{.PackageType}})
{{if eq .PackageType "meson"}}# Для Meson проектов:
meson setup build --prefix=/usr --buildtype=release
ninja -C build -j $(nproc){{else if eq .PackageType "cpp"}}# Для C/C++ проектов:
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
make -j$(nproc){{else if eq .PackageType "go"}}# Для Go проектов:
go build -buildmode=pie -trimpath -ldflags "-s -w" -o {{.Name}}{{else if eq .PackageType "python"}}# Для Python проектов:
python -m build --wheel --no-isolation{{else if eq .PackageType "nodejs"}}# Для Node.js проектов:
npm ci --production
npm run build{{else if eq .PackageType "rust"}}# Для Rust проектов:
cargo build --release --locked{{else if eq .PackageType "git"}}# Для Git проектов (обычно исходный код):
make -j$(nproc){{else}}# Стандартная сборка:
make -j$(nproc){{end}}{{end}}
}{{else}}# Бинарный пакет - сборка не требуется{{end}}
package() {
cd "$srcdir"{{if .PackageFunc}}
# Из PKGBUILD (адаптировано для ALR):
{{.PackageFunc}}
# Автоматически сгенерированные команды установки:
{{.GenerateInstallCommands}}{{else}}
# TODO: Адаптируйте установку файлов под конкретный проект {{.Name}}
{{if eq .PackageType "meson"}}# Для Meson проектов:
meson install -C build --destdir="$pkgdir"{{else if eq .PackageType "cpp"}}# Для C/C++ проектов:
cd build
make DESTDIR="$pkgdir" install{{else if eq .PackageType "go"}}# Для Go проектов:
# Исполняемый файл уже собран в корне{{else if eq .PackageType "python"}}# Для Python проектов:
pip install --root="$pkgdir/" . --no-deps --disable-pip-version-check{{else if eq .PackageType "nodejs"}}# Для Node.js проектов:
npm install -g --prefix="$pkgdir/usr" .{{else if eq .PackageType "rust"}}# Для Rust проектов:
# Исполняемый файл в target/release/{{else if eq .PackageType "bin"}}# Бинарный пакет:
# Файлы уже распакованы{{else}}# Стандартная установка:
make DESTDIR="$pkgdir" install{{end}}
# Автоматически сгенерированные команды установки:
{{.GenerateInstallCommands}}{{end}}
}

View File

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

View File

@@ -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) 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 { if len(result) == 0 {
result, err = rs.db.GetPkgs(ctx, "name LIKE ?", pkgName) result, err = rs.db.GetPkgs(ctx, "name LIKE ?", pkgName)
} }

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

@@ -0,0 +1,106 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package stats
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"time"
)
type InstallationData struct {
PackageName string `json:"packageName"`
Version string `json:"version,omitempty"`
InstallType string `json:"installType"` // "install" or "upgrade"
UserAgent string `json:"userAgent"`
Fingerprint string `json:"fingerprint,omitempty"`
}
var (
apiEndpoints = []string{
"https://alr.plemya-x.ru/api/packages/track-install",
"http://localhost:3001/api/packages/track-install",
}
userAgent = "ALR-CLI/1.0"
)
func generateFingerprint(packageName string) string {
hostname, _ := os.Hostname()
data := fmt.Sprintf("%s_%s_%s", hostname, packageName, time.Now().Format("2006-01-02"))
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:])
}
// TrackInstallation отправляет статистику установки пакета
func TrackInstallation(ctx context.Context, packageName string, installType string) {
// Запускаем в отдельной горутине, чтобы не блокировать основной процесс
go func() {
data := InstallationData{
PackageName: packageName,
InstallType: installType,
UserAgent: userAgent,
Fingerprint: generateFingerprint(packageName),
}
jsonData, err := json.Marshal(data)
if err != nil {
return // Тихо игнорируем ошибки - статистика не критична
}
// Пробуем отправить запрос к разным endpoint-ам
for _, endpoint := range apiEndpoints {
if sendRequest(endpoint, jsonData) {
return // Если хотя бы один запрос прошёл успешно, выходим
}
}
}()
}
func sendRequest(endpoint string, data []byte) bool {
client := &http.Client{
Timeout: 5 * time.Second,
}
req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(data))
if err != nil {
return false
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("User-Agent", userAgent)
resp, err := client.Do(req)
if err != nil {
return false
}
defer resp.Body.Close()
return resp.StatusCode >= 200 && resp.StatusCode < 300
}
// ShouldTrackPackage проверяет, нужно ли отслеживать установку этого пакета
func ShouldTrackPackage(packageName string) bool {
// Отслеживаем только alr-bin
return strings.Contains(packageName, "alr-bin")
}

View File

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

View File

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

View File

@@ -35,7 +35,6 @@ import (
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" "gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
) )
@@ -60,9 +59,6 @@ func ListCmd() *cli.Command {
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
return err
}
ctx := c.Context ctx := c.Context

View File

@@ -87,7 +87,6 @@ func GetApp() *cli.App {
// Internal commands // Internal commands
InternalBuildCmd(), InternalBuildCmd(),
InternalInstallCmd(), InternalInstallCmd(),
InternalMountCmd(),
InternalReposCmd(), InternalReposCmd(),
}, },
Before: func(c *cli.Context) error { Before: func(c *cli.Context) error {

View File

@@ -280,14 +280,14 @@ func handleCache(cacheDir, dest, name string, t Type) (bool, error) {
cd.Close() cd.Close()
if slices.Contains(names, name) { if slices.Contains(names, name) {
err = os.Link(filepath.Join(cacheDir, name), dest) err = linkOrCopy(filepath.Join(cacheDir, name), dest)
if err != nil { if err != nil {
return false, err return false, err
} }
return true, nil return true, nil
} }
case TypeDir: case TypeDir:
err := linkDir(cacheDir, dest) err := linkOrCopyDir(cacheDir, dest)
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -296,8 +296,40 @@ func handleCache(cacheDir, dest, name string, t Type) (bool, error) {
return false, nil return false, nil
} }
// Функция linkDir рекурсивно создает жесткие ссылки для файлов из каталога src в каталог dest // linkOrCopy пытается создать жесткую ссылку, а если не получается - копирует файл
func linkDir(src, dest string) error { func linkOrCopy(src, dest string) error {
err := os.Link(src, dest)
if err != nil {
// Если не удалось создать ссылку, копируем файл
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
destFile, err := os.Create(dest)
if err != nil {
return err
}
defer destFile.Close()
_, err = io.Copy(destFile, srcFile)
if err != nil {
return err
}
// Копируем права доступа
srcInfo, err := srcFile.Stat()
if err != nil {
return err
}
return os.Chmod(dest, srcInfo.Mode())
}
return nil
}
// linkOrCopyDir рекурсивно создает жесткие ссылки или копирует файлы из каталога src в каталог dest
func linkOrCopyDir(src, dest string) error {
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error { return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
@@ -317,7 +349,7 @@ func linkDir(src, dest string) error {
return os.MkdirAll(newPath, info.Mode()) return os.MkdirAll(newPath, info.Mode())
} }
return os.Link(path, newPath) return linkOrCopy(path, newPath)
}) })
} }

View File

@@ -61,7 +61,8 @@ func (dc *DownloadCache) New(ctx context.Context, id string) (string, error) {
} }
} }
err = os.MkdirAll(itemPath, 0o755) // Создаем директорию с правильными правами (различается для prod и тестов)
err = createDir(itemPath, 0o2775)
if err != nil { if err != nil {
return "", err return "", err
} }

View File

@@ -0,0 +1,37 @@
//go:build !test
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package dlcache
import (
"os"
"strings"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
)
// createDir создает директорию с правильными правами для production
func createDir(itemPath string, mode os.FileMode) error {
// Используем специальную функцию для создания каталогов с setgid битом только для /tmp/alr
// В остальных случаях используем обычное создание директории
if strings.HasPrefix(itemPath, "/tmp/alr") {
return utils.EnsureTempDirWithRootOwner(itemPath, mode)
} else {
return os.MkdirAll(itemPath, mode)
}
}

View File

@@ -45,7 +45,7 @@ func (c *TestALRConfig) GetPaths() *config.Paths {
func prepare(t *testing.T) *TestALRConfig { func prepare(t *testing.T) *TestALRConfig {
t.Helper() t.Helper()
dir, err := os.MkdirTemp("/tmp", "alr-dlcache-test.*") dir, err := os.MkdirTemp("", "alr-dlcache-test.*")
if err != nil { if err != nil {
panic(err) panic(err)
} }
@@ -57,7 +57,7 @@ func prepare(t *testing.T) *TestALRConfig {
func cleanup(t *testing.T, cfg *TestALRConfig) { func cleanup(t *testing.T, cfg *TestALRConfig) {
t.Helper() t.Helper()
os.Remove(cfg.CacheDir) os.RemoveAll(cfg.CacheDir)
} }
func TestNew(t *testing.T) { func TestNew(t *testing.T) {
@@ -82,6 +82,12 @@ func TestNew(t *testing.T) {
fi, err := os.Stat(dir) fi, err := os.Stat(dir)
if err != nil { if err != nil {
t.Errorf("stat: expected no error, got %s", err) t.Errorf("stat: expected no error, got %s", err)
return
}
if fi == nil {
t.Errorf("Expected file info to not be nil")
return
} }
if !fi.IsDir() { if !fi.IsDir() {

View 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)
}

View File

@@ -21,13 +21,14 @@ package types
// Config represents the ALR configuration file // Config represents the ALR configuration file
type Config struct { type Config struct {
RootCmd string `json:"rootCmd" koanf:"rootCmd"` RootCmd string `json:"rootCmd" koanf:"rootCmd"`
UseRootCmd bool `json:"useRootCmd" koanf:"useRootCmd"` UseRootCmd bool `json:"useRootCmd" koanf:"useRootCmd"`
PagerStyle string `json:"pagerStyle" koanf:"pagerStyle"` PagerStyle string `json:"pagerStyle" koanf:"pagerStyle"`
IgnorePkgUpdates []string `json:"ignorePkgUpdates" koanf:"ignorePkgUpdates"` IgnorePkgUpdates []string `json:"ignorePkgUpdates" koanf:"ignorePkgUpdates"`
Repos []Repo `json:"repo" koanf:"repo"` Repos []Repo `json:"repo" koanf:"repo"`
AutoPull bool `json:"autoPull" koanf:"autoPull"` AutoPull bool `json:"autoPull" koanf:"autoPull"`
LogLevel string `json:"logLevel" koanf:"logLevel"` LogLevel string `json:"logLevel" koanf:"logLevel"`
UpdateSystemOnUpgrade bool `json:"updateSystemOnUpgrade" koanf:"updateSystemOnUpgrade"`
} }
// Repo represents a ALR repo within a configuration file // Repo represents a ALR repo within a configuration file

View File

@@ -21,7 +21,6 @@ import (
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
) )
func RefreshCmd() *cli.Command { func RefreshCmd() *cli.Command {
@@ -30,9 +29,6 @@ func RefreshCmd() *cli.Command {
Usage: gotext.Get("Pull all repositories that have changed"), Usage: gotext.Get("Pull all repositories that have changed"),
Aliases: []string{"ref"}, Aliases: []string{"ref"},
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
ctx := c.Context ctx := c.Context

View File

@@ -114,9 +114,6 @@ func RemoveRepoCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
} }
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
deps, err = appbuilder. deps, err = appbuilder.
New(ctx). New(ctx).

37
scripts/fmt-precommit.sh Executable file
View 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
View 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

View File

@@ -32,12 +32,20 @@ error() {
installPkg() { installPkg() {
rootCmd="" rootCmd=""
if command -v doas &>/dev/null; then
rootCmd="doas" # Проверяем, запущен ли скрипт от root
elif command -v sudo &>/dev/null; then if [ "$(id -u)" = "0" ]; then
rootCmd="sudo" # Если root, не используем sudo/doas
rootCmd=""
else else
warn "Не обнаружена команда повышения привилегий (например, sudo, doas)" # Если не root, ищем команду повышения привилегий
if command -v doas &>/dev/null; then
rootCmd="doas"
elif command -v sudo &>/dev/null; then
rootCmd="sudo"
else
warn "Не обнаружена команда повышения привилегий (например, sudo, doas)"
fi
fi fi
case $1 in case $1 in
@@ -48,10 +56,46 @@ installPkg() {
esac esac
} }
trackInstallation() {
# Отправить статистику установки (не критично если не получится)
if command -v curl &>/dev/null; then
# Генерируем уникальный отпечаток на основе hostname и даты
fingerprint=$(echo "$(hostname)_$(date +%Y-%m-%d)" | sha256sum 2>/dev/null | cut -d' ' -f1 || echo "$(hostname)_$(date +%Y-%m-%d)")
# Пробуем разные домены/порты для отправки статистики
for api_url in "https://alr.plemya-x.ru/api/packages/track-install" "http://localhost:3001/api/packages/track-install"; do
curl -s -m 5 -X POST "$api_url" \
-H "Content-Type: application/json" \
-H "User-Agent: ALR-InstallScript/1.0" \
-d "{
\"packageName\": \"alr-bin\",
\"installType\": \"script\",
\"userAgent\": \"ALR-InstallScript/1.0\",
\"fingerprint\": \"$fingerprint\"
}" >/dev/null 2>&1
# Если один запрос удался, не пробуем остальные
if [ $? -eq 0 ]; then
break
fi
done
fi
}
if ! command -v curl &>/dev/null; then if ! command -v curl &>/dev/null; then
error "Этот скрипт требует команду curl. Пожалуйста, установите её и запустите снова." error "Этот скрипт требует команду curl. Пожалуйста, установите её и запустите снова."
fi fi
# Определение архитектуры системы
arch=$(uname -m)
case $arch in
x86_64) debArch="amd64"; rpmArch="x86_64" ;;
aarch64) debArch="arm64"; rpmArch="aarch64" ;;
armv7l) debArch="armhf"; rpmArch="armv7hl" ;;
*) error "Неподдерживаемая архитектура: $arch" ;;
esac
info "Обнаружена архитектура: $arch"
pkgFormat="" pkgFormat=""
pkgMgr="" pkgMgr=""
if command -v pacman &>/dev/null; then if command -v pacman &>/dev/null; then
@@ -88,25 +132,50 @@ else
fi fi
if [ -z "$noPkgMgr" ]; then if [ -z "$noPkgMgr" ]; then
info "Получение списка файлов с https://gitea.plemya-x.ru/Plemya-x/ALR/releases" info "Получение списка релизов через API Gitea"
# Изменено URL и регулярное выражение для списка файлов # Используем API для получения последнего релиза
pageContent=$(curl -s https://gitea.plemya-x.ru/Plemya-x/ALR/releases) releases=$(curl -s "https://gitea.plemya-x.ru/api/v1/repos/Plemya-x/ALR/releases")
# Извлечение списка файлов из HTML if [ -z "$releases" ] || [ "$releases" = "null" ]; then
fileList=$(echo "$pageContent" | grep -oP '(?<=href=").*?(?=")' | grep -E 'alr-bin.*\.(pkg.tar.zst|rpm|deb)') error "Не удалось получить список релизов. Проверьте соединение с интернетом."
fi
echo "Полученный список файлов:" # Получаем URL последнего релиза
echo "$fileList" latestReleaseUrl=$(echo "$releases" | grep -o '"browser_download_url":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$latestReleaseUrl" ]; then
# Fallback на парсинг HTML если API не работает
warn "API не доступен, пробуем получить список через HTML"
pageContent=$(curl -s https://gitea.plemya-x.ru/Plemya-x/ALR/releases)
fileList=$(echo "$pageContent" | grep -oP '(?<=href=")[^"]*alr-bin[^"]*\.(pkg\.tar\.zst|rpm|deb)' | sed 's|^|https://gitea.plemya-x.ru|')
else
# Получаем список файлов из API
latestReleaseId=$(echo "$releases" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
assets=$(curl -s "https://gitea.plemya-x.ru/api/v1/repos/Plemya-x/ALR/releases/$latestReleaseId/assets")
# Фильтруем только пакеты, исключая tar.gz архивы
fileList=$(echo "$assets" | grep -o '"browser_download_url":"[^"]*"' | cut -d'"' -f4 | grep -v '\.tar\.gz$')
fi
if [ -z "$fileList" ]; then
warn "Не найдены готовые пакеты в последнем релизе"
warn "Возможно, для вашего дистрибутива нужно собрать пакет из исходников"
warn "Инструкции по сборке: https://gitea.plemya-x.ru/Plemya-x/ALR"
error "Не удалось получить список пакетов для загрузки"
fi
info "Получен список файлов релиза"
if [ "$pkgMgr" == "pacman" ]; then if [ "$pkgMgr" == "pacman" ]; then
latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*\.pkg\.tar\.zst' | sort -V | tail -n 1) latestFile=$(echo "$fileList" | grep -E "alr-bin.*\.pkg\.tar\.zst" | sort -V | tail -n 1)
elif [ "$pkgMgr" == "apt" ]; then elif [ "$pkgMgr" == "apt" ]; then
latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*\.amd64\.deb' | sort -V | tail -n 1) latestFile=$(echo "$fileList" | grep -E "alr-bin.*\.${debArch}\.deb" | sort -V | tail -n 1)
elif [[ "$pkgMgr" == "dnf" || "$pkgMgr" == "yum" || "$pkgMgr" == "zypper" ]]; then elif [[ "$pkgMgr" == "dnf" || "$pkgMgr" == "yum" || "$pkgMgr" == "zypper" ]]; then
latestFile=$(printf "%s\n" "${fileList[@]}" | grep -E 'alr-bin-.*\.x86_64\.rpm' | grep -v 'alt[0-9]*' | sort -V | tail -n 1) latestFile=$(echo "$fileList" | grep -E "alr-bin.*\.${rpmArch}\.rpm" | grep -v 'alt[0-9]*' | sort -V | tail -n 1)
elif [ "$pkgMgr" == "apt-get" ]; then elif [ "$pkgMgr" == "apt-get" ]; then
latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*-alt[0-9]+\.x86_64\.rpm' | sort -V | tail -n 1) latestFile=$(echo "$fileList" | grep -E "alr-bin.*-alt[0-9]+\.${rpmArch}\.rpm" | sort -V | tail -n 1)
elif [ "$pkgMgr" == "apk" ]; then
latestFile=$(echo "$fileList" | grep -E "alr-bin.*\.apk" | sort -V | tail -n 1)
else else
error "Не поддерживаемый менеджер пакетов для автоматической установки" error "Не поддерживаемый менеджер пакетов для автоматической установки"
fi fi
@@ -119,18 +188,35 @@ if [ -z "$noPkgMgr" ]; then
fname="$(mktemp -u -p /tmp "alr.XXXXXXXXXX").${pkgFormat}" fname="$(mktemp -u -p /tmp "alr.XXXXXXXXXX").${pkgFormat}"
info "Загрузка пакета ALR" # Настраиваем trap для очистки временного файла
curl -o $fname -L "$latestFile" trap "rm -f $fname" EXIT
if [ ! -f "$fname" ]; then info "Загрузка пакета ALR"
error "Ошибка загрузки пакета ALR" info "URL: $latestFile"
# Загружаем с проверкой кода возврата
if ! curl -f -L -o "$fname" "$latestFile"; then
error "Ошибка загрузки пакета ALR. Проверьте подключение к интернету."
fi fi
# Проверяем что файл не пустой
if [ ! -s "$fname" ]; then
error "Загруженный файл пустой или поврежден"
fi
# Показываем размер загруженного файла
fileSize=$(du -h "$fname" | cut -f1)
info "Загружен пакет размером $fileSize"
info "Установка пакета ALR" info "Установка пакета ALR"
installPkg "$pkgMgr" "$fname" installPkg "$pkgMgr" "$fname"
# Отправляем статистику установки
trackInstallation
info "Очистка" info "Очистка"
rm "$fname" rm -f "$fname"
trap - EXIT
info "Готово!" info "Готово!"
else else

View 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

View File

@@ -29,7 +29,6 @@ import (
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" "gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/search" "gitea.plemya-x.ru/Plemya-x/ALR/internal/search"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
) )
@@ -72,9 +71,6 @@ func SearchCmd() *cli.Command {
}, },
}, },
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
return err
}
ctx := c.Context ctx := c.Context

View File

@@ -55,9 +55,6 @@ func UpgradeCmd() *cli.Command {
}, },
}, },
Action: utils.RootNeededAction(func(c *cli.Context) error { Action: utils.RootNeededAction(func(c *cli.Context) error {
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
installer, installerClose, err := build.GetSafeInstaller() installer, installerClose, err := build.GetSafeInstaller()
if err != nil { if err != nil {
@@ -65,9 +62,6 @@ func UpgradeCmd() *cli.Command {
} }
defer installerClose() defer installerClose()
if err := utils.ExitIfCantSetNoNewPrivs(); err != nil {
return err
}
scripter, scripterClose, err := build.GetSafeScriptExecutor() scripter, scripterClose, err := build.GetSafeScriptExecutor()
if err != nil { if err != nil {
@@ -90,6 +84,19 @@ func UpgradeCmd() *cli.Command {
} }
defer deps.Defer() defer deps.Defer()
// Обновляем систему, если это включено в конфигурации
if deps.Cfg.UpdateSystemOnUpgrade() {
slog.Info(gotext.Get("Updating system packages..."))
err = deps.Manager.UpgradeAll(&manager.Opts{
NoConfirm: !c.Bool("interactive"),
Args: manager.Args,
})
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error updating system packages"), err)
}
slog.Info(gotext.Get("System packages updated successfully"))
}
builder, err := build.NewMainBuilder( builder, err := build.NewMainBuilder(
deps.Cfg, deps.Cfg,
deps.Manager, deps.Manager,