25 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
aa08c04e0c fix: use single output format for alt list and alr list -I 2025-07-09 20:38:24 +03:00
54 changed files with 1918 additions and 590 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

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

Before

Width:  |  Height:  |  Size: 926 B

After

Width:  |  Height:  |  Size: 926 B

View File

@@ -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 {

View File

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

View File

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

View File

@@ -0,0 +1,50 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//go:build e2e
package e2etests_test
import (
"testing"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue62List(t *testing.T) {
runMatrixSuite(
t,
"issue-62-list",
COMMON_SYSTEMS,
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", "alr-repo", "bd26236cd7")
execShouldNoError(t, r, "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "in", "foo-pkg")
r.Command("alr", "list", "-I").
ExpectSuccess().
ExpectStdoutMatchesSnapshot().
Run(t)
r.Command("alr", "list").
ExpectSuccess().
ExpectStdoutMatchesSnapshot().
Run(t)
},
)
}

116
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,12 +72,18 @@ 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 {
if os.IsNotExist(err) {
// Директория не существует, просто создадим её позже
slog.Info(gotext.Get("Cache directory does not exist, will create it"))
} else {
return cliutils.FormatCliExit(gotext.Get("Unable to open cache directory"), err) return cliutils.FormatCliExit(gotext.Get("Unable to open cache directory"), err)
} }
} else {
defer dir.Close() defer dir.Close()
entries, err := dir.Readdirnames(-1) entries, err := dir.Readdirnames(-1)
@@ -73,23 +94,106 @@ func FixCmd() *cli.Command {
for _, entry := range entries { for _, entry := range entries {
fullPath := filepath.Join(paths.CacheDir, entry) fullPath := filepath.Join(paths.CacheDir, entry)
// Пробуем сделать файлы доступными для записи
if err := makeWritableRecursive(fullPath); err != nil { if err := makeWritableRecursive(fullPath); err != nil {
slog.Debug("Failed to make path writable", "path", fullPath, "error", err) slog.Debug("Failed to make path writable", "path", fullPath, "error", err)
} }
// Пробуем удалить
err = os.RemoveAll(fullPath) err = os.RemoveAll(fullPath)
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 cache item (%s) as current user, trying with sudo", entry))
sudoCmd := execWithPrivileges("rm", "-rf", fullPath)
if sudoErr := sudoCmd.Run(); sudoErr != nil {
// Если и через sudo не получилось, пропускаем с предупреждением
slog.Error(gotext.Get("Unable to remove cache item (%s)", entry), "error", err)
continue
}
}
}
}
// Очищаем временные директории
slog.Info(gotext.Get("Clearing temporary directory"))
tmpDir := "/tmp/alr"
if _, err := os.Stat(tmpDir); err == nil {
// Директория существует, пробуем очистить
err = os.RemoveAll(tmpDir)
if err != nil {
// Если не получилось удалить, пробуем через sudo
slog.Warn(gotext.Get("Unable to remove temporary directory as current user, trying with sudo"))
sudoCmd := execWithPrivileges("rm", "-rf", tmpDir)
if sudoErr := sudoCmd.Run(); sudoErr != nil {
slog.Error(gotext.Get("Unable to remove temporary directory"), "error", err)
}
}
}
// Создаем базовый каталог /tmp/alr с владельцем root:wheel и правами 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 {
// Если не получилось, пробуем через 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) 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.
New(ctx). New(ctx).
WithConfig(). WithConfig().

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,11 +402,21 @@ 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 {
// Исключаем зависимости, которые являются подпакетами текущего мультипакета
if _, isCurrentPackage := currentPackageNames[d]; !isCurrentPackage {
filteredDepends = append(filteredDepends, d) filteredDepends = append(filteredDepends, d)
} }
} }
}
slog.Debug("BuildALRDeps") slog.Debug("BuildALRDeps")
newBuiltDeps, repoDeps, err := b.BuildALRDeps(ctx, input, filteredDepends) newBuiltDeps, repoDeps, err := b.BuildALRDeps(ctx, input, filteredDepends)
@@ -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 {
// Проверяем, являются ли пакеты подпакетами одного мультипакета
if isMultiPackage(pkgs) && verb == "install" {
// Для мультипакетов при установке ВСЕГДА берем все подпакеты без выбора
// Это правильное поведение как для прямой установки, так и для зависимостей
outPkgs = append(outPkgs, pkgs...)
} else if interactive {
// Для разных пакетов с одинаковым именем - показываем меню выбора
choice, err := PkgPrompt(ctx, pkgs, verb, interactive) choice, err := PkgPrompt(ctx, pkgs, verb, interactive)
if err != nil { if err != nil {
slog.Error(gotext.Get("Error prompting for choice of package")) slog.Error(gotext.Get("Error prompting for choice of package"))
os.Exit(1) os.Exit(1)
} }
outPkgs = append(outPkgs, choice) outPkgs = append(outPkgs, choice)
} else if len(pkgs) == 1 || !interactive { } else {
// Если не интерактивный режим - берем первый
outPkgs = append(outPkgs, pkgs[0])
}
} else {
// Если только один пакет - берем его
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

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

View File

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

View File

@@ -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

@@ -445,30 +445,34 @@ msgstr ""
msgid "You need to be root to perform this action" msgid "You need to be root to perform this action"
msgstr "" msgstr ""
#: list.go:43 #: list.go:45
msgid "List ALR repo packages" msgid "List ALR repo packages"
msgstr "" msgstr ""
#: list.go:57 #: list.go:59
msgid "Format output using a Go template" msgid "Format output using a Go template"
msgstr "" msgstr ""
#: list.go:89 #: list.go:91
msgid "Error getting packages for upgrade" msgid "Error getting packages for upgrade"
msgstr "" msgstr ""
#: list.go:92 #: list.go:94
msgid "No packages for upgrade" msgid "No packages for upgrade"
msgstr "" msgstr ""
#: list.go:102 list.go:184 #: list.go:104 list.go:201
msgid "Error parsing format template" msgid "Error parsing format template"
msgstr "" msgstr ""
#: list.go:108 list.go:188 #: list.go:110 list.go:205
msgid "Error executing template" msgid "Error executing template"
msgstr "" msgstr ""
#: list.go:164
msgid "Failed to parse release"
msgstr ""
#: main.go:45 #: main.go:45
msgid "Print the current ALR version and exit" msgid "Print the current ALR version and exit"
msgstr "" msgstr ""

View File

@@ -5,15 +5,15 @@
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: unnamed project\n" "Project-Id-Version: unnamed project\n"
"PO-Revision-Date: 2025-06-29 21:05+0300\n" "PO-Revision-Date: 2025-07-09 20:38+0300\n"
"Last-Translator: Maxim Slipenko <maks1ms@alt-gnome.ru>\n" "Last-Translator: Maxim Slipenko <maks1ms@alt-gnome.ru>\n"
"Language-Team: Russian\n" "Language-Team: Russian\n"
"Language: ru\n" "Language: ru\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Gtranslator 48.0\n" "X-Generator: Gtranslator 48.0\n"
#: build.go:41 #: build.go:41
@@ -404,8 +404,8 @@ msgid ""
"This command is deprecated and would be removed in the future, use \"%s\" " "This command is deprecated and would be removed in the future, use \"%s\" "
"instead!" "instead!"
msgstr "" msgstr ""
"Эта команда устарела и будет удалена в будущем, используйте вместо нее \"%s" "Эта команда устарела и будет удалена в будущем, используйте вместо нее "
"\"!" "\"%s\"!"
#: internal/db/db.go:76 #: internal/db/db.go:76
msgid "Database version mismatch; resetting" msgid "Database version mismatch; resetting"
@@ -461,30 +461,34 @@ msgstr "Вы должны быть членом %s чтобы выполнить
msgid "You need to be root to perform this action" msgid "You need to be root to perform this action"
msgstr "Вы должны быть root чтобы выполнить это" msgstr "Вы должны быть root чтобы выполнить это"
#: list.go:43 #: list.go:45
msgid "List ALR repo packages" msgid "List ALR repo packages"
msgstr "Список пакетов репозитория ALR" msgstr "Список пакетов репозитория ALR"
#: list.go:57 #: list.go:59
msgid "Format output using a Go template" msgid "Format output using a Go template"
msgstr "Формат выходных данных с использованием шаблона Go" msgstr "Формат выходных данных с использованием шаблона Go"
#: list.go:89 #: list.go:91
msgid "Error getting packages for upgrade" msgid "Error getting packages for upgrade"
msgstr "Ошибка при получении пакетов для обновления" msgstr "Ошибка при получении пакетов для обновления"
#: list.go:92 #: list.go:94
msgid "No packages for upgrade" msgid "No packages for upgrade"
msgstr "Нет пакетов к обновлению" msgstr "Нет пакетов к обновлению"
#: list.go:102 list.go:184 #: list.go:104 list.go:201
msgid "Error parsing format template" msgid "Error parsing format template"
msgstr "Ошибка при разборе шаблона" msgstr "Ошибка при разборе шаблона"
#: list.go:108 list.go:188 #: list.go:110 list.go:205
msgid "Error executing template" msgid "Error executing template"
msgstr "Ошибка при выполнении шаблона" msgstr "Ошибка при выполнении шаблона"
#: list.go:164
msgid "Failed to parse release"
msgstr "Не удалось разобрать релиз"
#: main.go:45 #: main.go:45
msgid "Print the current ALR version and exit" msgid "Print the current ALR version and exit"
msgstr "Показать текущую версию ALR и выйти" msgstr "Показать текущую версию ALR и выйти"

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

41
list.go
View File

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

View File

@@ -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

@@ -28,6 +28,7 @@ type Config struct {
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,6 +32,13 @@ error() {
installPkg() { installPkg() {
rootCmd="" rootCmd=""
# Проверяем, запущен ли скрипт от root
if [ "$(id -u)" = "0" ]; then
# Если root, не используем sudo/doas
rootCmd=""
else
# Если не root, ищем команду повышения привилегий
if command -v doas &>/dev/null; then if command -v doas &>/dev/null; then
rootCmd="doas" rootCmd="doas"
elif command -v sudo &>/dev/null; then elif command -v sudo &>/dev/null; then
@@ -39,6 +46,7 @@ installPkg() {
else else
warn "Не обнаружена команда повышения привилегий (например, sudo, doas)" warn "Не обнаружена команда повышения привилегий (например, sudo, doas)"
fi fi
fi
case $1 in case $1 in
pacman) $rootCmd pacman --noconfirm -U "${@:2}" ;; pacman) $rootCmd pacman --noconfirm -U "${@:2}" ;;
@@ -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 для получения последнего релиза
releases=$(curl -s "https://gitea.plemya-x.ru/api/v1/repos/Plemya-x/ALR/releases")
if [ -z "$releases" ] || [ "$releases" = "null" ]; then
error "Не удалось получить список релизов. Проверьте соединение с интернетом."
fi
# Получаем URL последнего релиза
latestReleaseUrl=$(echo "$releases" | grep -o '"browser_download_url":"[^"]*"' | head -1 | cut -d'"' -f4)
if [ -z "$latestReleaseUrl" ]; then
# Fallback на парсинг HTML если API не работает
warn "API не доступен, пробуем получить список через HTML"
pageContent=$(curl -s https://gitea.plemya-x.ru/Plemya-x/ALR/releases) pageContent=$(curl -s https://gitea.plemya-x.ru/Plemya-x/ALR/releases)
fileList=$(echo "$pageContent" | grep -oP '(?<=href=")[^"]*alr-bin[^"]*\.(pkg\.tar\.zst|rpm|deb)' | sed 's|^|https://gitea.plemya-x.ru|')
else
# Получаем список файлов из API
latestReleaseId=$(echo "$releases" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
assets=$(curl -s "https://gitea.plemya-x.ru/api/v1/repos/Plemya-x/ALR/releases/$latestReleaseId/assets")
# Фильтруем только пакеты, исключая tar.gz архивы
fileList=$(echo "$assets" | grep -o '"browser_download_url":"[^"]*"' | cut -d'"' -f4 | grep -v '\.tar\.gz$')
fi
# Извлечение списка файлов из HTML if [ -z "$fileList" ]; then
fileList=$(echo "$pageContent" | grep -oP '(?<=href=").*?(?=")' | grep -E 'alr-bin.*\.(pkg.tar.zst|rpm|deb)') warn "Не найдены готовые пакеты в последнем релизе"
warn "Возможно, для вашего дистрибутива нужно собрать пакет из исходников"
warn "Инструкции по сборке: https://gitea.plemya-x.ru/Plemya-x/ALR"
error "Не удалось получить список пакетов для загрузки"
fi
echo "Полученный список файлов:" info "Получен список файлов релиза"
echo "$fileList"
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,