forked from Plemya-x/ALR
Compare commits
25 Commits
Author | SHA1 | Date | |
---|---|---|---|
86a982478e | |||
8bc82cb95c | |||
9783ce37de | |||
b852688ab0 | |||
2ff5e6f7b6 | |||
c9639b7073 | |||
c1847e1191 | |||
f2b0f57c12 | |||
59cc41e94c | |||
75ece6dfcc | |||
6af712f1d5 | |||
bad225c6b1 | |||
4b3bf44aaa | |||
67b3c40430 | |||
4948e6b8fc | |||
292125a8ff | |||
77055aa2cb | |||
737bf68f95 | |||
1089e8a3f3 | |||
aa42ab0607 | |||
51fa7ca6fb | |||
ab41700004 | |||
7cb1bc9548 | |||
07187da423 | |||
802fe2b0b2 |
@@ -19,7 +19,7 @@ name: Pre-commit
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
|
||||
|
||||
|
@@ -47,7 +47,7 @@ jobs:
|
||||
|
||||
- name: Prepare for install
|
||||
run: |
|
||||
apt-get update && apt-get install -y libcap2-bin bindfs
|
||||
apt-get update
|
||||
|
||||
- name: Build alr
|
||||
env:
|
||||
@@ -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/release='[0-9]\+'/release='1'/g" alr-default/alr-bin/alr.sh
|
||||
|
||||
# - name: Install alr
|
||||
# run: |
|
||||
# make install
|
||||
#
|
||||
# # temporary fix
|
||||
# groupadd wheel
|
||||
# usermod -aG wheel root
|
||||
|
||||
# - name: Build packages
|
||||
# run: |
|
||||
# SCRIPT_PATH=alr-default/alr-bin/alr.sh
|
||||
# ALR_DISTRO=altlinux ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH"
|
||||
# ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH"
|
||||
# ALR_PKG_FORMAT=deb alr build -s "$SCRIPT_PATH"
|
||||
# ALR_PKG_FORMAT=archlinux alr build -s "$SCRIPT_PATH"
|
||||
|
||||
# - name: Upload assets
|
||||
# uses: akkuman/gitea-release-action@v1
|
||||
# with:
|
||||
# body: ${{ steps.changes.outputs.changes }}
|
||||
# files: |-
|
||||
# alr-bin+alr-default_${{ env.VERSION }}-1.red80_amd64.deb \
|
||||
# alr-bin+alr-default-${{ env.VERSION }}-1-x86_64.pkg.tar.zst \
|
||||
# alr-bin+alr-default-${{ env.VERSION }}-1.red80.x86_64.rpm \
|
||||
# alr-bin+alr-default-${{ env.VERSION }}-alt1.x86_64.rpm
|
||||
|
||||
- name: Commit changes
|
||||
- name: Install alr
|
||||
env:
|
||||
CREATE_SYSTEM_RESOURCES: 0
|
||||
run: |
|
||||
cd alr-default
|
||||
git config user.name "gitea"
|
||||
git config user.email "admin@plemya-x.ru"
|
||||
git add .
|
||||
git commit -m "Обновление версии до ${{ env.VERSION }}"
|
||||
git push
|
||||
make install
|
||||
|
||||
- name: Prepare directories for ALR
|
||||
run: |
|
||||
# Создаём необходимые директории для работы alr build
|
||||
mkdir -p /tmp/alr/dl /tmp/alr/pkgs /var/cache/alr
|
||||
chmod -R 777 /tmp/alr
|
||||
chmod -R 755 /var/cache/alr
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
SCRIPT_PATH=alr-default/alr-bin/alr.sh
|
||||
ALR_DISTRO=altlinux ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH"
|
||||
ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH"
|
||||
ALR_PKG_FORMAT=deb alr build -s "$SCRIPT_PATH"
|
||||
ALR_PKG_FORMAT=archlinux alr build -s "$SCRIPT_PATH"
|
||||
|
||||
- name: Upload assets
|
||||
uses: akkuman/gitea-release-action@v1
|
||||
with:
|
||||
body: ${{ steps.changes.outputs.changes }}
|
||||
files: |-
|
||||
alr-bin*.deb
|
||||
alr-bin*.rpm
|
||||
alr-bin*.pkg.tar.zst
|
||||
|
@@ -1,69 +0,0 @@
|
||||
# ALR - Any Linux Repository
|
||||
# Copyright (C) 2025 The ALR Authors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
name: Update alr-git
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
changelog:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install the latest version of uv
|
||||
uses: astral-sh/setup-uv@v6
|
||||
|
||||
- name: Setup alr-spec
|
||||
run: |
|
||||
uv tool install alr-spec==0.0.5
|
||||
|
||||
- name: Install alr
|
||||
run: |
|
||||
apt-get update && apt-get install -y libcap2-bin
|
||||
curl -fsS https://gitea.plemya-x.ru/Plemya-x/ALR/raw/branch/master/scripts/install.sh | bash
|
||||
|
||||
- name: Checkout this repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set ALR version
|
||||
run: |
|
||||
echo "NEW_ALR_VERSION=$(alr helper git-version)" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout alr-default repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: Plemya-x/alr-default
|
||||
token: ${{ secrets.GITEAPUBLIC }}
|
||||
path: alr-default
|
||||
|
||||
- name: Update version
|
||||
working-directory: ./alr-default/alr-git
|
||||
run: |
|
||||
alr-spec set-field version $NEW_ALR_VERSION
|
||||
alr-spec set-field release 1
|
||||
|
||||
- name: Commit changes
|
||||
run: |
|
||||
cd alr-default
|
||||
git config user.name "gitea"
|
||||
git config user.email "admin@plemya-x.ru"
|
||||
git add .
|
||||
git commit -m "Обновление версии до $NEW_ALR_VERSION"
|
||||
git push
|
7
.gitignore
vendored
7
.gitignore
vendored
@@ -3,11 +3,12 @@
|
||||
/cmd/alr-api-server/alr-api-server
|
||||
/dist/
|
||||
/internal/config/version.txt
|
||||
.fleet
|
||||
.idea
|
||||
.gigaide
|
||||
.fleet/
|
||||
.idea/
|
||||
.gigaide/
|
||||
|
||||
*.out
|
||||
|
||||
e2e-tests/alr
|
||||
CLAUDE.md
|
||||
commit_msg.txt
|
@@ -19,13 +19,13 @@ repos:
|
||||
hooks:
|
||||
- id: test-coverage
|
||||
name: Run test coverage
|
||||
entry: make test-coverage
|
||||
entry: bash scripts/test-coverage-precommit.sh
|
||||
language: system
|
||||
pass_filenames: false
|
||||
|
||||
- id: fmt
|
||||
name: Format code
|
||||
entry: make fmt
|
||||
entry: bash scripts/fmt-precommit.sh
|
||||
language: system
|
||||
pass_filenames: false
|
||||
|
||||
@@ -37,6 +37,7 @@ repos:
|
||||
|
||||
- id: i18n
|
||||
name: Update i18n
|
||||
entry: make i18n
|
||||
entry: bash scripts/i18n-precommit.sh
|
||||
language: system
|
||||
pass_filenames: false
|
||||
always_run: true
|
13
Makefile
13
Makefile
@@ -49,17 +49,12 @@ install: \
|
||||
$(INSTALLED_BIN): $(BIN)
|
||||
install -Dm755 $< $@
|
||||
ifeq ($(CREATE_SYSTEM_RESOURCES),1)
|
||||
setcap cap_setuid,cap_setgid+ep $(INSTALLED_BIN)
|
||||
@if id alr >/dev/null 2>&1; then \
|
||||
echo "User 'alr' already exists. Skipping."; \
|
||||
else \
|
||||
useradd -r -s /usr/sbin/nologin alr; \
|
||||
fi
|
||||
@for dir in $(ROOT_DIRS); do \
|
||||
install -d -o alr -g alr -m 755 $$dir; \
|
||||
install -d -m 775 $$dir; \
|
||||
chgrp wheel $$dir; \
|
||||
done
|
||||
else
|
||||
@echo "Skipping user and root dir creation (CREATE_SYSTEM_RESOURCES=0)"
|
||||
@echo "Skipping root dir creation (CREATE_SYSTEM_RESOURCES=0)"
|
||||
endif
|
||||
|
||||
$(INSTALLED_BASH_COMPLETION): $(BASH_COMPLETION)
|
||||
@@ -93,7 +88,7 @@ i18n:
|
||||
bash scripts/i18n-badge.sh
|
||||
|
||||
test-coverage:
|
||||
go test ./... -v -coverpkg=./... -coverprofile=coverage.out
|
||||
go test -tags=test ./... -v -coverpkg=./... -coverprofile=coverage.out
|
||||
bash scripts/coverage-badge.sh
|
||||
|
||||
update-deps-cve:
|
||||
|
12
README.md
12
README.md
@@ -44,7 +44,7 @@ ALR был создан потому, что упаковка программн
|
||||
|
||||
## Документация
|
||||
|
||||
Документация находится в [Wiki](https://disc.plemya-x.ru/c/alr/wiki-alr).
|
||||
Документация находится в [Wiki](https://alr.plemya-x.ru/wiki/ALR).
|
||||
|
||||
---
|
||||
|
||||
@@ -52,23 +52,21 @@ ALR был создан потому, что упаковка программн
|
||||
|
||||
Репозитории alr - это git-хранилища, которые содержат каталог для каждого пакета с файлом `alr.sh` внутри. Файл `alr.sh` содержит все инструкции по сборке пакета и информацию о нем. Скрипты `alr.sh` аналогичны скриптам Aur PKGBUILD.
|
||||
|
||||
Например, репозиторий с ALR [Plemya-x/alr-default](https://gitea.plemya-x.ru/Plemya-x/alr-default.git)
|
||||
Например, репозиторий с ALR [alr-default](https://gitea.plemya-x.ru/Plemya-x/alr-default.git)
|
||||
```
|
||||
alr repo add alr-default https://gitea.plemya-x.ru/Plemya-x/alr-default.git
|
||||
```
|
||||
Репозиторий пакетов [Plemya-x/alr-repo](https://gitea.plemya-x.ru/Plemya-x/alr-repo.git) можно подключить так:
|
||||
Репозиторий пакетов [alr-repo](https://gitea.plemya-x.ru/Plemya-x/alr-repo.git) можно подключить так:
|
||||
```
|
||||
alr repo add alr-repo https://gitea.plemya-x.ru/Plemya-x/alr-repo.git
|
||||
```
|
||||
Репозиторий Linux-Gaming [Plemya-x/alr-LG](https://gitea.plemya-x.ru/Plemya-x/alr-LG.git) можно подключить так:
|
||||
Репозиторий Linux-Gaming [alr-LG](https://gitea.plemya-x.ru/Plemya-x/alr-LG.git) можно подключить так:
|
||||
```
|
||||
alr repo add alr-LG https://gitea.plemya-x.ru/Plemya-x/alr-LG.git
|
||||
alr repo add alr-LG https://git.linux-gaming.ru/Linux-Gaming/alr-LG.git
|
||||
```
|
||||
|
||||
---
|
||||
## Соцсети
|
||||
VK - https://vk.com/plemya_kh
|
||||
|
||||
Telegram - https://t.me/plemyakh
|
||||
|
||||
## Спасибы
|
||||
|
24
build.go
24
build.go
@@ -72,12 +72,6 @@ func BuildCmd() *cli.Command {
|
||||
return cliutils.FormatCliExit(gotext.Get("Error getting working directory"), err)
|
||||
}
|
||||
|
||||
wd, wdCleanup, err := Mount(wd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wdCleanup()
|
||||
|
||||
ctx := c.Context
|
||||
|
||||
deps, err := appbuilder.
|
||||
@@ -156,19 +150,9 @@ func BuildCmd() *cli.Command {
|
||||
return cliutils.FormatCliExit(gotext.Get("Nothing to build"), nil)
|
||||
}
|
||||
|
||||
if scriptArgs != nil {
|
||||
scriptFile := filepath.Base(scriptArgs.Script)
|
||||
newScriptDir, scriptDirCleanup, err := Mount(filepath.Dir(scriptArgs.Script))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer scriptDirCleanup()
|
||||
scriptArgs.Script = filepath.Join(newScriptDir, scriptFile)
|
||||
}
|
||||
|
||||
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
|
||||
installer, installerClose, err := build.GetSafeInstaller()
|
||||
if err != nil {
|
||||
@@ -176,9 +160,7 @@ func BuildCmd() *cli.Command {
|
||||
}
|
||||
defer installerClose()
|
||||
|
||||
if err := utils.ExitIfCantSetNoNewPrivs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
scripter, scripterClose, err := build.GetSafeScriptExecutor()
|
||||
if err != nil {
|
||||
|
@@ -76,6 +76,7 @@ var configKeys = []string{
|
||||
"autoPull",
|
||||
"logLevel",
|
||||
"ignorePkgUpdates",
|
||||
"updateSystemOnUpgrade",
|
||||
}
|
||||
|
||||
func SetConfig() *cli.Command {
|
||||
@@ -137,6 +138,12 @@ func SetConfig() *cli.Command {
|
||||
}
|
||||
}
|
||||
deps.Cfg.System.SetIgnorePkgUpdates(updates)
|
||||
case "updateSystemOnUpgrade":
|
||||
boolValue, err := strconv.ParseBool(value)
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit(gotext.Get("invalid boolean value for %s: %s", key, value), err)
|
||||
}
|
||||
deps.Cfg.System.SetUpdateSystemOnUpgrade(boolValue)
|
||||
case "repo", "repos":
|
||||
return cliutils.FormatCliExit(gotext.Get("use 'repo add/remove' commands to manage repositories"), nil)
|
||||
default:
|
||||
@@ -206,6 +213,8 @@ func GetConfig() *cli.Command {
|
||||
} else {
|
||||
fmt.Println(strings.Join(updates, ", "))
|
||||
}
|
||||
case "updateSystemOnUpgrade":
|
||||
fmt.Println(deps.Cfg.UpdateSystemOnUpgrade())
|
||||
case "repo", "repos":
|
||||
repos := deps.Cfg.Repos()
|
||||
if len(repos) == 0 {
|
||||
|
146
fix.go
146
fix.go
@@ -23,6 +23,7 @@ import (
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
@@ -33,14 +34,28 @@ import (
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
|
||||
)
|
||||
|
||||
// execWithPrivileges выполняет команду напрямую если root или CI, иначе через sudo
|
||||
func execWithPrivileges(name string, args ...string) *exec.Cmd {
|
||||
isRoot := os.Geteuid() == 0
|
||||
isCI := os.Getenv("CI") == "true"
|
||||
|
||||
if !isRoot && !isCI {
|
||||
// Если не root и не в CI, используем sudo
|
||||
allArgs := append([]string{name}, args...)
|
||||
return exec.Command("sudo", allArgs...)
|
||||
} else {
|
||||
// Если root или в CI, запускаем напрямую
|
||||
return exec.Command(name, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func FixCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "fix",
|
||||
Usage: gotext.Get("Attempt to fix problems with ALR"),
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Команда выполняется от текущего пользователя
|
||||
// При необходимости будет запрошен sudo для удаления файлов root
|
||||
|
||||
ctx := c.Context
|
||||
|
||||
@@ -57,37 +72,126 @@ func FixCmd() *cli.Command {
|
||||
|
||||
paths := cfg.GetPaths()
|
||||
|
||||
slog.Info(gotext.Get("Clearing cache directory"))
|
||||
slog.Info(gotext.Get("Clearing cache and temporary directories"))
|
||||
|
||||
// Проверяем, существует ли директория кэша
|
||||
dir, err := os.Open(paths.CacheDir)
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit(gotext.Get("Unable to open cache directory"), err)
|
||||
}
|
||||
defer dir.Close()
|
||||
if os.IsNotExist(err) {
|
||||
// Директория не существует, просто создадим её позже
|
||||
slog.Info(gotext.Get("Cache directory does not exist, will create it"))
|
||||
} else {
|
||||
return cliutils.FormatCliExit(gotext.Get("Unable to open cache directory"), err)
|
||||
}
|
||||
} else {
|
||||
defer dir.Close()
|
||||
|
||||
entries, err := dir.Readdirnames(-1)
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit(gotext.Get("Unable to read cache directory contents"), err)
|
||||
}
|
||||
|
||||
for _, entry := range entries {
|
||||
fullPath := filepath.Join(paths.CacheDir, entry)
|
||||
|
||||
if err := makeWritableRecursive(fullPath); err != nil {
|
||||
slog.Debug("Failed to make path writable", "path", fullPath, "error", err)
|
||||
entries, err := dir.Readdirnames(-1)
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit(gotext.Get("Unable to read cache directory contents"), err)
|
||||
}
|
||||
|
||||
err = os.RemoveAll(fullPath)
|
||||
for _, entry := range entries {
|
||||
fullPath := filepath.Join(paths.CacheDir, entry)
|
||||
|
||||
// Пробуем сделать файлы доступными для записи
|
||||
if err := makeWritableRecursive(fullPath); err != nil {
|
||||
slog.Debug("Failed to make path writable", "path", fullPath, "error", err)
|
||||
}
|
||||
|
||||
// Пробуем удалить
|
||||
err = os.RemoveAll(fullPath)
|
||||
if err != nil {
|
||||
// Если не получилось удалить, пробуем через sudo
|
||||
slog.Warn(gotext.Get("Unable to remove cache item (%s) as current user, trying with sudo", entry))
|
||||
|
||||
sudoCmd := execWithPrivileges("rm", "-rf", fullPath)
|
||||
if sudoErr := sudoCmd.Run(); sudoErr != nil {
|
||||
// Если и через sudo не получилось, пропускаем с предупреждением
|
||||
slog.Error(gotext.Get("Unable to remove cache item (%s)", entry), "error", err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Очищаем временные директории
|
||||
slog.Info(gotext.Get("Clearing temporary directory"))
|
||||
tmpDir := "/tmp/alr"
|
||||
if _, err := os.Stat(tmpDir); err == nil {
|
||||
// Директория существует, пробуем очистить
|
||||
err = os.RemoveAll(tmpDir)
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit(gotext.Get("Unable to remove cache item (%s)", entry), err)
|
||||
// Если не получилось удалить, пробуем через sudo
|
||||
slog.Warn(gotext.Get("Unable to remove temporary directory as current user, trying with sudo"))
|
||||
sudoCmd := execWithPrivileges("rm", "-rf", tmpDir)
|
||||
if sudoErr := sudoCmd.Run(); sudoErr != nil {
|
||||
slog.Error(gotext.Get("Unable to remove temporary directory"), "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Создаем базовый каталог /tmp/alr с владельцем root:wheel и правами 775
|
||||
err = utils.EnsureTempDirWithRootOwner(tmpDir, 0o775)
|
||||
if err != nil {
|
||||
slog.Warn(gotext.Get("Unable to create temporary directory"), "error", err)
|
||||
}
|
||||
|
||||
// Создаем каталог dl с правами для группы wheel
|
||||
dlDir := filepath.Join(tmpDir, "dl")
|
||||
err = utils.EnsureTempDirWithRootOwner(dlDir, 0o775)
|
||||
if err != nil {
|
||||
slog.Warn(gotext.Get("Unable to create download directory"), "error", err)
|
||||
}
|
||||
|
||||
// Создаем каталог pkgs с правами для группы wheel
|
||||
pkgsDir := filepath.Join(tmpDir, "pkgs")
|
||||
err = utils.EnsureTempDirWithRootOwner(pkgsDir, 0o775)
|
||||
if err != nil {
|
||||
slog.Warn(gotext.Get("Unable to create packages directory"), "error", err)
|
||||
}
|
||||
|
||||
// Исправляем права на все существующие файлы в /tmp/alr, если там что-то есть
|
||||
if _, err := os.Stat(tmpDir); err == nil {
|
||||
slog.Info(gotext.Get("Fixing permissions on temporary files"))
|
||||
|
||||
// Проверяем, есть ли файлы в директории
|
||||
entries, err := os.ReadDir(tmpDir)
|
||||
if err == nil && len(entries) > 0 {
|
||||
fixCmd := execWithPrivileges("chown", "-R", "root:wheel", tmpDir)
|
||||
if fixErr := fixCmd.Run(); fixErr != nil {
|
||||
slog.Warn(gotext.Get("Unable to fix file ownership"), "error", fixErr)
|
||||
}
|
||||
|
||||
fixCmd = execWithPrivileges("chmod", "-R", "2775", tmpDir)
|
||||
if fixErr := fixCmd.Run(); fixErr != nil {
|
||||
slog.Warn(gotext.Get("Unable to fix file permissions"), "error", fixErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
slog.Info(gotext.Get("Rebuilding cache"))
|
||||
|
||||
err = os.MkdirAll(paths.CacheDir, 0o755)
|
||||
// Пробуем создать директорию кэша
|
||||
err = os.MkdirAll(paths.CacheDir, 0o775)
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit(gotext.Get("Unable to create new cache directory"), err)
|
||||
// Если не получилось, пробуем через sudo с правильными правами для группы wheel
|
||||
slog.Info(gotext.Get("Creating cache directory with sudo"))
|
||||
sudoCmd := execWithPrivileges("mkdir", "-p", paths.CacheDir)
|
||||
if sudoErr := sudoCmd.Run(); sudoErr != nil {
|
||||
return cliutils.FormatCliExit(gotext.Get("Unable to create new cache directory"), err)
|
||||
}
|
||||
|
||||
// Устанавливаем права 775 и группу wheel
|
||||
chmodCmd := execWithPrivileges("chmod", "775", paths.CacheDir)
|
||||
if chmodErr := chmodCmd.Run(); chmodErr != nil {
|
||||
return cliutils.FormatCliExit(gotext.Get("Unable to set cache directory permissions"), chmodErr)
|
||||
}
|
||||
|
||||
chgrpCmd := execWithPrivileges("chgrp", "wheel", paths.CacheDir)
|
||||
if chgrpErr := chgrpCmd.Run(); chgrpErr != nil {
|
||||
return cliutils.FormatCliExit(gotext.Get("Unable to set cache directory group"), chgrpErr)
|
||||
}
|
||||
}
|
||||
|
||||
deps, err = appbuilder.
|
||||
|
23
gen.go
23
gen.go
@@ -61,6 +61,29 @@ func GenCmd() *cli.Command {
|
||||
})
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "aur",
|
||||
Usage: gotext.Get("Generate a ALR script for an AUR package"),
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Required: true,
|
||||
Usage: gotext.Get("Name of the AUR package"),
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "version",
|
||||
Aliases: []string{"v"},
|
||||
Usage: gotext.Get("Version of the package (optional, uses latest if not specified)"),
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
return gen.AUR(os.Stdout, gen.AUROptions{
|
||||
Name: c.String("name"),
|
||||
Version: c.String("version"),
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
8
info.go
8
info.go
@@ -31,7 +31,6 @@ import (
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
|
||||
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
|
||||
)
|
||||
@@ -48,9 +47,6 @@ func InfoCmd() *cli.Command {
|
||||
},
|
||||
},
|
||||
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
|
||||
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := c.Context
|
||||
deps, err := appbuilder.
|
||||
@@ -74,9 +70,7 @@ func InfoCmd() *cli.Command {
|
||||
return nil
|
||||
}),
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Запуск от текущего пользователя
|
||||
|
||||
args := c.Args()
|
||||
if args.Len() < 1 {
|
||||
|
@@ -51,9 +51,6 @@ func InstallCmd() *cli.Command {
|
||||
return cliutils.FormatCliExit(gotext.Get("Command install expected at least 1 argument, got %d", args.Len()), nil)
|
||||
}
|
||||
|
||||
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
installer, installerClose, err := build.GetSafeInstaller()
|
||||
if err != nil {
|
||||
@@ -61,9 +58,6 @@ func InstallCmd() *cli.Command {
|
||||
}
|
||||
defer installerClose()
|
||||
|
||||
if err := utils.ExitIfCantSetNoNewPrivs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scripter, scripterClose, err := build.GetSafeScriptExecutor()
|
||||
if err != nil {
|
||||
@@ -116,9 +110,6 @@ func InstallCmd() *cli.Command {
|
||||
return nil
|
||||
}),
|
||||
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
|
||||
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := c.Context
|
||||
deps, err := appbuilder.
|
||||
|
163
internal.go
163
internal.go
@@ -17,14 +17,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
@@ -36,7 +30,6 @@ import (
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
|
||||
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
|
||||
@@ -52,9 +45,6 @@ func InternalBuildCmd() *cli.Command {
|
||||
|
||||
slog.Debug("start _internal-safe-script-executor", "uid", syscall.Getuid(), "gid", syscall.Getgid())
|
||||
|
||||
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := config.New()
|
||||
err := cfg.Load()
|
||||
@@ -92,9 +82,6 @@ func InternalReposCmd() *cli.Command {
|
||||
Action: utils.RootNeededAction(func(ctx *cli.Context) error {
|
||||
logger.SetupForGoPlugin()
|
||||
|
||||
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deps, err := appbuilder.
|
||||
New(ctx.Context).
|
||||
@@ -129,16 +116,7 @@ func InternalInstallCmd() *cli.Command {
|
||||
Action: func(c *cli.Context) error {
|
||||
logger.SetupForGoPlugin()
|
||||
|
||||
if err := utils.EnsureIsAlrUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Before escalating the rights, we made sure that
|
||||
// this is an ALR user, so it looks safe.
|
||||
err := utils.EscalateToRootUid()
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit("cannot escalate to root", err)
|
||||
}
|
||||
// Запуск от текущего пользователя, повышение прав будет через sudo при необходимости
|
||||
|
||||
deps, err := appbuilder.
|
||||
New(c.Context).
|
||||
@@ -175,143 +153,4 @@ func InternalInstallCmd() *cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func Mount(target string) (string, func(), error) {
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to get executable path: %w", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(exe, "_internal-temporary-mount", target)
|
||||
|
||||
stdoutPipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to get stdout pipe: %w", err)
|
||||
}
|
||||
|
||||
stdinPipe, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("failed to get stdin pipe: %w", err)
|
||||
}
|
||||
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return "", nil, fmt.Errorf("failed to start mount: %w", err)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdoutPipe)
|
||||
var mountPath string
|
||||
if scanner.Scan() {
|
||||
mountPath = scanner.Text()
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
return "", nil, fmt.Errorf("failed to read mount output: %w", err)
|
||||
}
|
||||
|
||||
if mountPath == "" {
|
||||
_ = cmd.Process.Kill()
|
||||
return "", nil, errors.New("mount failed: no target path returned")
|
||||
}
|
||||
|
||||
cleanup := func() {
|
||||
slog.Debug("cleanup triggered")
|
||||
_, _ = fmt.Fprintln(stdinPipe, "")
|
||||
_ = cmd.Wait()
|
||||
}
|
||||
|
||||
return mountPath, cleanup, nil
|
||||
}
|
||||
|
||||
func InternalMountCmd() *cli.Command {
|
||||
return &cli.Command{
|
||||
Name: "_internal-temporary-mount",
|
||||
HideHelp: true,
|
||||
Hidden: true,
|
||||
Action: func(c *cli.Context) error {
|
||||
logger.SetupForGoPlugin()
|
||||
|
||||
sourceDir := c.Args().First()
|
||||
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit("cannot get current user", err)
|
||||
}
|
||||
|
||||
_, alrGid, err := utils.GetUidGidAlrUser()
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit("cannot get alr user", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(sourceDir); err != nil {
|
||||
return cliutils.FormatCliExit(fmt.Sprintf("cannot read %s", sourceDir), err)
|
||||
}
|
||||
|
||||
if err := utils.EnuseIsPrivilegedGroupMember(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Before escalating the rights, we made sure that
|
||||
// 1. user in wheel group
|
||||
// 2. user can access sourceDir
|
||||
if err := utils.EscalateToRootUid(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syscall.Setgid(alrGid); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(constants.AlrRunDir, 0o770); err != nil {
|
||||
return cliutils.FormatCliExit(fmt.Sprintf("failed to create %s", constants.AlrRunDir), err)
|
||||
}
|
||||
|
||||
if err := os.Chown(constants.AlrRunDir, 0, alrGid); err != nil {
|
||||
return cliutils.FormatCliExit(fmt.Sprintf("failed to chown %s", constants.AlrRunDir), err)
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(constants.AlrRunDir, fmt.Sprintf("bindfs-%d", os.Getpid()))
|
||||
// 0750: owner (root) and group (alr)
|
||||
if err := os.MkdirAll(targetDir, 0o750); err != nil {
|
||||
return cliutils.FormatCliExit("error creating bindfs target directory", err)
|
||||
}
|
||||
|
||||
// chown AlrRunDir/mounts/bindfs-* to (root:alr),
|
||||
// so alr user can access dir
|
||||
if err := os.Chown(targetDir, 0, alrGid); err != nil {
|
||||
return cliutils.FormatCliExit("failed to chown bindfs directory", err)
|
||||
}
|
||||
|
||||
bindfsCmd := exec.Command(
|
||||
"bindfs",
|
||||
fmt.Sprintf("--map=%s/alr:@%s/@alr", u.Uid, u.Gid),
|
||||
sourceDir,
|
||||
targetDir,
|
||||
)
|
||||
|
||||
bindfsCmd.Stderr = os.Stderr
|
||||
|
||||
if err := bindfsCmd.Run(); err != nil {
|
||||
return cliutils.FormatCliExit("failed to strart bindfs", err)
|
||||
}
|
||||
|
||||
fmt.Println(targetDir)
|
||||
|
||||
_, _ = bufio.NewReader(os.Stdin).ReadString('\n')
|
||||
|
||||
slog.Debug("start unmount", "dir", targetDir)
|
||||
|
||||
umountCmd := exec.Command("umount", targetDir)
|
||||
umountCmd.Stderr = os.Stderr
|
||||
if err := umountCmd.Run(); err != nil {
|
||||
return cliutils.FormatCliExit(fmt.Sprintf("failed to unmount %s", targetDir), err)
|
||||
}
|
||||
|
||||
if err := os.Remove(targetDir); err != nil {
|
||||
return cliutils.FormatCliExit(fmt.Sprintf("error removing directory %s", targetDir), err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@@ -32,6 +32,7 @@ import (
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/stats"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
|
||||
@@ -319,9 +320,9 @@ func (b *Builder) BuildPackage(
|
||||
}
|
||||
|
||||
var builtDeps []*BuiltDep
|
||||
var remainingVars []*alrsh.Package
|
||||
|
||||
if !input.opts.Clean {
|
||||
var remainingVars []*alrsh.Package
|
||||
for _, vars := range varsOfPackages {
|
||||
builtPkgPath, ok, err := b.cacheExecutor.CheckForBuiltPackage(ctx, input, vars)
|
||||
if err != nil {
|
||||
@@ -330,6 +331,7 @@ func (b *Builder) BuildPackage(
|
||||
if ok {
|
||||
builtDeps = append(builtDeps, &BuiltDep{
|
||||
Path: builtPkgPath,
|
||||
Name: vars.Name,
|
||||
})
|
||||
} else {
|
||||
remainingVars = append(remainingVars, vars)
|
||||
@@ -337,8 +339,12 @@ func (b *Builder) BuildPackage(
|
||||
}
|
||||
|
||||
if len(remainingVars) == 0 {
|
||||
slog.Info(gotext.Get("Using cached package"), "name", basePkg)
|
||||
return builtDeps, nil
|
||||
}
|
||||
|
||||
// Обновляем varsOfPackages только теми пакетами, которые нужно собрать
|
||||
varsOfPackages = remainingVars
|
||||
}
|
||||
|
||||
slog.Debug("ViewScript")
|
||||
@@ -401,9 +407,19 @@ func (b *Builder) BuildPackage(
|
||||
|
||||
// We filter so as not to re-build what has already been built at the `installBuildDeps` stage.
|
||||
var filteredDepends []string
|
||||
|
||||
// Создаем набор подпакетов текущего мультипакета для исключения циклических зависимостей
|
||||
currentPackageNames := make(map[string]struct{})
|
||||
for _, pkg := range input.packages {
|
||||
currentPackageNames[pkg] = struct{}{}
|
||||
}
|
||||
|
||||
for _, d := range depends {
|
||||
if _, found := depNames[d]; !found {
|
||||
filteredDepends = append(filteredDepends, d)
|
||||
// Исключаем зависимости, которые являются подпакетами текущего мультипакета
|
||||
if _, isCurrentPackage := currentPackageNames[d]; !isCurrentPackage {
|
||||
filteredDepends = append(filteredDepends, d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,6 +544,13 @@ func (b *Builder) InstallALRPackages(
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Отслеживание установки ALR пакетов
|
||||
for _, dep := range res {
|
||||
if stats.ShouldTrackPackage(dep.Name) {
|
||||
stats.TrackInstallation(ctx, dep.Name, "upgrade")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -552,11 +575,13 @@ func (b *Builder) BuildALRDeps(
|
||||
repoDeps = notFound
|
||||
|
||||
// Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез
|
||||
pkgs := cliutils.FlattenPkgs(
|
||||
// Для зависимостей указываем isDependency = true
|
||||
pkgs := cliutils.FlattenPkgsWithContext(
|
||||
ctx,
|
||||
found,
|
||||
"install",
|
||||
input.BuildOpts().Interactive,
|
||||
true,
|
||||
)
|
||||
type item struct {
|
||||
pkg *alrsh.Package
|
||||
@@ -691,6 +716,13 @@ func (i *Builder) InstallPkgs(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Отслеживание установки локальных пакетов
|
||||
for _, dep := range builtDeps {
|
||||
if stats.ShouldTrackPackage(dep.Name) {
|
||||
stats.TrackInstallation(ctx, dep.Name, "install")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(repoDeps) > 0 {
|
||||
@@ -700,6 +732,13 @@ func (i *Builder) InstallPkgs(
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Отслеживание установки пакетов из репозитория
|
||||
for _, pkg := range repoDeps {
|
||||
if stats.ShouldTrackPackage(pkg) {
|
||||
stats.TrackInstallation(ctx, pkg, "install")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builtDeps, nil
|
||||
|
@@ -44,9 +44,9 @@ var HandshakeConfig = plugin.HandshakeConfig{
|
||||
|
||||
func setCommonCmdEnv(cmd *exec.Cmd) {
|
||||
cmd.Env = []string{
|
||||
"HOME=/var/cache/alr",
|
||||
"LOGNAME=alr",
|
||||
"USER=alr",
|
||||
"HOME=" + os.Getenv("HOME"),
|
||||
"LOGNAME=" + os.Getenv("USER"),
|
||||
"USER=" + os.Getenv("USER"),
|
||||
"PATH=/usr/bin:/bin:/usr/local/bin",
|
||||
}
|
||||
for _, env := range os.Environ() {
|
||||
@@ -102,9 +102,7 @@ func getSafeExecutor[T any](subCommand, pluginName string) (T, func(), error) {
|
||||
Cmd: cmd,
|
||||
Logger: logger.GetHCLoggerAdapter(),
|
||||
SkipHostEnv: true,
|
||||
UnixSocketConfig: &plugin.UnixSocketConfig{
|
||||
Group: "alr",
|
||||
},
|
||||
UnixSocketConfig: &plugin.UnixSocketConfig{},
|
||||
SyncStderr: os.Stderr,
|
||||
})
|
||||
rpcClient, err := client.Client()
|
||||
|
@@ -23,6 +23,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
|
||||
)
|
||||
@@ -74,7 +75,9 @@ func (s *SourceDownloader) DownloadSources(
|
||||
}
|
||||
}
|
||||
|
||||
opts.DlCache = dlcache.New(s.cfg.GetPaths().CacheDir)
|
||||
// Используем временную директорию для загрузок
|
||||
// dlcache.New добавит свой подкаталог "dl" внутри
|
||||
opts.DlCache = dlcache.New(constants.TempDir)
|
||||
|
||||
err := dl.Download(ctx, opts)
|
||||
if err != nil {
|
||||
|
@@ -19,6 +19,7 @@ package build
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@@ -40,6 +41,7 @@ import (
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
|
||||
@@ -47,15 +49,22 @@ import (
|
||||
|
||||
// Функция prepareDirs подготавливает директории для сборки.
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
err = os.MkdirAll(dirs.SrcDir, 0o755) // Создаем директорию для источников
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.MkdirAll(dirs.PkgDir, 0o755) // Создаем директорию для пакетов
|
||||
|
||||
// Создаем директорию для пакетов с setgid битом
|
||||
return utils.EnsureTempDirWithRootOwner(dirs.PkgDir, 0o2775)
|
||||
}
|
||||
|
||||
// Функция buildContents создает секцию содержимого пакета, которая содержит файлы,
|
||||
|
@@ -103,22 +103,62 @@ func ShowScript(path, name, style string) error {
|
||||
// FlattenPkgs attempts to flatten the a map of slices of packages into a single slice
|
||||
// of packages by prompting the user if multiple packages match.
|
||||
func FlattenPkgs(ctx context.Context, found map[string][]alrsh.Package, verb string, interactive bool) []alrsh.Package {
|
||||
return FlattenPkgsWithContext(ctx, found, verb, interactive, false)
|
||||
}
|
||||
|
||||
// FlattenPkgsWithContext расширенная версия FlattenPkgs с контекстом обработки зависимостей
|
||||
func FlattenPkgsWithContext(ctx context.Context, found map[string][]alrsh.Package, verb string, interactive bool, isDependency bool) []alrsh.Package {
|
||||
var outPkgs []alrsh.Package
|
||||
for _, pkgs := range found {
|
||||
if len(pkgs) > 1 && interactive {
|
||||
choice, err := PkgPrompt(ctx, pkgs, verb, interactive)
|
||||
if err != nil {
|
||||
slog.Error(gotext.Get("Error prompting for choice of package"))
|
||||
os.Exit(1)
|
||||
if len(pkgs) > 1 {
|
||||
// Проверяем, являются ли пакеты подпакетами одного мультипакета
|
||||
if isMultiPackage(pkgs) && verb == "install" {
|
||||
// Для мультипакетов при установке ВСЕГДА берем все подпакеты без выбора
|
||||
// Это правильное поведение как для прямой установки, так и для зависимостей
|
||||
outPkgs = append(outPkgs, pkgs...)
|
||||
} else if interactive {
|
||||
// Для разных пакетов с одинаковым именем - показываем меню выбора
|
||||
choice, err := PkgPrompt(ctx, pkgs, verb, interactive)
|
||||
if err != nil {
|
||||
slog.Error(gotext.Get("Error prompting for choice of package"))
|
||||
os.Exit(1)
|
||||
}
|
||||
outPkgs = append(outPkgs, choice)
|
||||
} else {
|
||||
// Если не интерактивный режим - берем первый
|
||||
outPkgs = append(outPkgs, pkgs[0])
|
||||
}
|
||||
outPkgs = append(outPkgs, choice)
|
||||
} else if len(pkgs) == 1 || !interactive {
|
||||
} else {
|
||||
// Если только один пакет - берем его
|
||||
outPkgs = append(outPkgs, pkgs[0])
|
||||
}
|
||||
}
|
||||
return outPkgs
|
||||
}
|
||||
|
||||
// isMultiPackage проверяет, являются ли пакеты подпакетами одного мультипакета
|
||||
func isMultiPackage(pkgs []alrsh.Package) bool {
|
||||
if len(pkgs) <= 1 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Проверяем, что у всех пакетов одинаковый BasePkgName и Repository
|
||||
firstBasePkg := pkgs[0].BasePkgName
|
||||
firstRepo := pkgs[0].Repository
|
||||
|
||||
if firstBasePkg == "" {
|
||||
return false // Не мультипакет
|
||||
}
|
||||
|
||||
for _, pkg := range pkgs[1:] {
|
||||
if pkg.BasePkgName != firstBasePkg || pkg.Repository != firstRepo {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// PkgPrompt asks the user to choose between multiple packages.
|
||||
func PkgPrompt(ctx context.Context, options []alrsh.Package, verb string, interactive bool) (alrsh.Package, error) {
|
||||
if !interactive {
|
||||
|
@@ -21,6 +21,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/goccy/go-yaml"
|
||||
@@ -55,7 +56,13 @@ func defaultConfigKoanf() *koanf.Koanf {
|
||||
"ignorePkgUpdates": []string{},
|
||||
"logLevel": "info",
|
||||
"autoPull": true,
|
||||
"repos": []types.Repo{},
|
||||
"updateSystemOnUpgrade": false,
|
||||
"repos": []types.Repo{
|
||||
{
|
||||
Name: "alr-default",
|
||||
URL: "https://gitea.plemya-x.ru/Plemya-x/alr-default.git",
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := k.Load(confmap.Provider(defaults, "."), nil); err != nil {
|
||||
panic(k)
|
||||
@@ -98,8 +105,20 @@ func (c *ALRConfig) Load() error {
|
||||
c.paths.UserConfigPath = constants.SystemConfigPath
|
||||
c.paths.CacheDir = constants.SystemCachePath
|
||||
c.paths.RepoDir = filepath.Join(c.paths.CacheDir, "repo")
|
||||
c.paths.PkgsDir = filepath.Join(c.paths.CacheDir, "pkgs")
|
||||
c.paths.DBPath = filepath.Join(c.paths.CacheDir, "db")
|
||||
c.paths.PkgsDir = filepath.Join(constants.TempDir, "pkgs") // Перемещаем в /tmp/alr/pkgs
|
||||
c.paths.DBPath = filepath.Join(c.paths.CacheDir, "alr.db")
|
||||
|
||||
// Проверяем существование кэш-директории, но не пытаемся создать
|
||||
if _, err := os.Stat(c.paths.CacheDir); err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("failed to check cache directory: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Выполняем миграцию конфигурации при необходимости
|
||||
if err := c.migrateConfig(); err != nil {
|
||||
return fmt.Errorf("failed to migrate config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -112,6 +131,45 @@ func (c *ALRConfig) ToYAML() (string, error) {
|
||||
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) PagerStyle() string { return c.cfg.PagerStyle }
|
||||
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) LogLevel() string { return c.cfg.LogLevel }
|
||||
func (c *ALRConfig) UseRootCmd() bool { return c.cfg.UseRootCmd }
|
||||
func (c *ALRConfig) UpdateSystemOnUpgrade() bool { return c.cfg.UpdateSystemOnUpgrade }
|
||||
func (c *ALRConfig) GetPaths() *Paths { return c.paths }
|
||||
|
@@ -142,3 +142,10 @@ func (c *SystemConfig) SetRepos(v []types.Repo) {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *SystemConfig) SetUpdateSystemOnUpgrade(v bool) {
|
||||
err := c.k.Set("updateSystemOnUpgrade", v)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
@@ -19,6 +19,6 @@ package constants
|
||||
const (
|
||||
SystemConfigPath = "/etc/alr/alr.toml"
|
||||
SystemCachePath = "/var/cache/alr"
|
||||
AlrRunDir = "/var/run/alr"
|
||||
TempDir = "/tmp/alr"
|
||||
PrivilegedGroup = "wheel"
|
||||
)
|
||||
|
@@ -21,7 +21,10 @@ package db
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
_ "modernc.org/sqlite"
|
||||
@@ -54,6 +57,21 @@ func New(config Config) *Database {
|
||||
|
||||
func (d *Database) Connect() error {
|
||||
dsn := d.config.GetPaths().DBPath
|
||||
|
||||
// Проверяем директорию для БД
|
||||
dbDir := filepath.Dir(dsn)
|
||||
if _, err := os.Stat(dbDir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Директория не существует - пытаемся создать
|
||||
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.SetLogLevel(log.LOG_DEBUG)
|
||||
// engine.ShowSQL(true)
|
||||
|
663
internal/gen/aur.go
Normal file
663
internal/gen/aur.go
Normal file
@@ -0,0 +1,663 @@
|
||||
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
|
||||
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
|
||||
//
|
||||
// ALR - Any Linux Repository
|
||||
// Copyright (C) 2025 The ALR Authors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package gen
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// Встраиваем шаблон для AUR пакетов
|
||||
//
|
||||
//go:embed tmpls/aur.tmpl.sh
|
||||
var aurTmpl string
|
||||
|
||||
// AUROptions содержит параметры для генерации шаблона AUR
|
||||
type AUROptions struct {
|
||||
Name string // Имя пакета в AUR
|
||||
Version string // Версия пакета (опционально, если не указана - берется последняя)
|
||||
CreateDir bool // Создавать ли директорию для пакета и дополнительные файлы
|
||||
}
|
||||
|
||||
// aurAPIResponse представляет структуру ответа от API AUR
|
||||
type aurAPIResponse struct {
|
||||
Version int `json:"version"` // Версия API
|
||||
Type string `json:"type"` // Тип ответа
|
||||
ResultCount int `json:"resultcount"` // Количество результатов
|
||||
Results []aurResult `json:"results"` // Массив результатов
|
||||
Error string `json:"error"` // Сообщение об ошибке (если есть)
|
||||
}
|
||||
|
||||
// aurResult содержит информацию о пакете из AUR
|
||||
type aurResult struct {
|
||||
ID int `json:"ID"`
|
||||
Name string `json:"Name"`
|
||||
PackageBaseID int `json:"PackageBaseID"`
|
||||
PackageBase string `json:"PackageBase"`
|
||||
Version string `json:"Version"`
|
||||
Description string `json:"Description"`
|
||||
URL string `json:"URL"`
|
||||
NumVotes int `json:"NumVotes"`
|
||||
Popularity float64 `json:"Popularity"`
|
||||
OutOfDate *int `json:"OutOfDate"`
|
||||
Maintainer string `json:"Maintainer"`
|
||||
FirstSubmitted int `json:"FirstSubmitted"`
|
||||
LastModified int `json:"LastModified"`
|
||||
URLPath string `json:"URLPath"`
|
||||
License []string `json:"License"`
|
||||
Keywords []string `json:"Keywords"`
|
||||
Depends []string `json:"Depends"`
|
||||
MakeDepends []string `json:"MakeDepends"`
|
||||
OptDepends []string `json:"OptDepends"`
|
||||
CheckDepends []string `json:"CheckDepends"`
|
||||
Conflicts []string `json:"Conflicts"`
|
||||
Provides []string `json:"Provides"`
|
||||
Replaces []string `json:"Replaces"`
|
||||
// Дополнительные поля для данных из PKGBUILD
|
||||
Sources []string `json:"-"`
|
||||
Checksums []string `json:"-"`
|
||||
BuildFunc string `json:"-"`
|
||||
PackageFunc string `json:"-"`
|
||||
PrepareFunc string `json:"-"`
|
||||
PackageType string `json:"-"` // python, go, rust, cpp, nodejs, bin, git
|
||||
HasDesktop bool `json:"-"` // Есть ли desktop файлы
|
||||
HasSystemd bool `json:"-"` // Есть ли systemd сервисы
|
||||
HasVersion bool `json:"-"` // Есть ли функция version()
|
||||
HasScripts []string `json:"-"` // Дополнительные скрипты (postinstall, postremove, etc)
|
||||
HasPatches bool `json:"-"` // Есть ли патчи
|
||||
Architectures []string `json:"-"` // Поддерживаемые архитектуры
|
||||
|
||||
// Автоматически определяемые файлы для install-* команд
|
||||
BinaryFiles []string `json:"-"` // Исполняемые файлы для install-binary
|
||||
LicenseFiles []string `json:"-"` // Лицензионные файлы для install-license
|
||||
ManualFiles []string `json:"-"` // Man страницы для install-manual
|
||||
DesktopFiles []string `json:"-"` // Desktop файлы для install-desktop
|
||||
ServiceFiles []string `json:"-"` // Systemd сервисы для install-systemd
|
||||
CompletionFiles map[string]string `json:"-"` // Файлы автодополнения по типу (bash, zsh, fish)
|
||||
}
|
||||
|
||||
// Вспомогательные методы для шаблона
|
||||
func (r aurResult) LicenseString() string {
|
||||
if len(r.License) == 0 {
|
||||
return "custom:Unknown"
|
||||
}
|
||||
// Форматируем лицензии для alr.sh
|
||||
licenses := make([]string, len(r.License))
|
||||
for i, l := range r.License {
|
||||
licenses[i] = fmt.Sprintf("'%s'", l)
|
||||
}
|
||||
return strings.Join(licenses, " ")
|
||||
}
|
||||
|
||||
func (r aurResult) DependsString() string {
|
||||
if len(r.Depends) == 0 {
|
||||
return ""
|
||||
}
|
||||
deps := make([]string, len(r.Depends))
|
||||
for i, d := range r.Depends {
|
||||
// Убираем версионные ограничения для простоты
|
||||
dep := strings.Split(d, ">=")[0]
|
||||
dep = strings.Split(dep, "<=")[0]
|
||||
dep = strings.Split(dep, "=")[0]
|
||||
dep = strings.Split(dep, ">")[0]
|
||||
dep = strings.Split(dep, "<")[0]
|
||||
deps[i] = fmt.Sprintf("'%s'", dep)
|
||||
}
|
||||
return strings.Join(deps, " ")
|
||||
}
|
||||
|
||||
func (r aurResult) MakeDependsString() string {
|
||||
if len(r.MakeDepends) == 0 {
|
||||
return ""
|
||||
}
|
||||
deps := make([]string, len(r.MakeDepends))
|
||||
for i, d := range r.MakeDepends {
|
||||
// Убираем версионные ограничения для простоты
|
||||
dep := strings.Split(d, ">=")[0]
|
||||
dep = strings.Split(dep, "<=")[0]
|
||||
dep = strings.Split(dep, "=")[0]
|
||||
dep = strings.Split(dep, ">")[0]
|
||||
dep = strings.Split(dep, "<")[0]
|
||||
deps[i] = fmt.Sprintf("'%s'", dep)
|
||||
}
|
||||
return strings.Join(deps, " ")
|
||||
}
|
||||
|
||||
func (r aurResult) GitURL() string {
|
||||
// Формируем URL для клонирования из AUR
|
||||
return fmt.Sprintf("https://aur.archlinux.org/%s.git", r.PackageBase)
|
||||
}
|
||||
|
||||
func (r aurResult) ArchitecturesString() string {
|
||||
if len(r.Architectures) == 0 {
|
||||
return "'all'"
|
||||
}
|
||||
archs := make([]string, len(r.Architectures))
|
||||
for i, arch := range r.Architectures {
|
||||
archs[i] = fmt.Sprintf("'%s'", arch)
|
||||
}
|
||||
return strings.Join(archs, " ")
|
||||
}
|
||||
|
||||
func (r aurResult) OptDependsString() string {
|
||||
if len(r.OptDepends) == 0 {
|
||||
return ""
|
||||
}
|
||||
optDeps := make([]string, 0, len(r.OptDepends))
|
||||
for _, dep := range r.OptDepends {
|
||||
// Форматируем опциональные зависимости для alr.sh
|
||||
parts := strings.SplitN(dep, ": ", 2)
|
||||
if len(parts) == 2 {
|
||||
optDeps = append(optDeps, fmt.Sprintf("'%s: %s'", parts[0], parts[1]))
|
||||
} else {
|
||||
optDeps = append(optDeps, fmt.Sprintf("'%s'", dep))
|
||||
}
|
||||
}
|
||||
return strings.Join(optDeps, "\n\t")
|
||||
}
|
||||
|
||||
func (r aurResult) ScriptsString() string {
|
||||
if len(r.HasScripts) == 0 {
|
||||
return ""
|
||||
}
|
||||
scripts := make([]string, len(r.HasScripts))
|
||||
for i, script := range r.HasScripts {
|
||||
scripts[i] = fmt.Sprintf("['%s']='%s.sh'", script, script)
|
||||
}
|
||||
return strings.Join(scripts, "\n\t")
|
||||
}
|
||||
|
||||
// GenerateInstallCommands генерирует команды install-* для шаблона
|
||||
func (r aurResult) GenerateInstallCommands() string {
|
||||
var commands []string
|
||||
|
||||
// install-binary команды
|
||||
for _, binary := range r.BinaryFiles {
|
||||
if binary == "./"+r.Name {
|
||||
commands = append(commands, fmt.Sprintf("\tinstall-binary %s", binary))
|
||||
} else {
|
||||
commands = append(commands, fmt.Sprintf("\tinstall-binary %s %s", binary, r.Name))
|
||||
}
|
||||
}
|
||||
|
||||
// install-license команды
|
||||
for _, license := range r.LicenseFiles {
|
||||
commands = append(commands, fmt.Sprintf("\tinstall-license %s %s/LICENSE", license, r.Name))
|
||||
}
|
||||
|
||||
// install-manual команды
|
||||
for _, manual := range r.ManualFiles {
|
||||
commands = append(commands, fmt.Sprintf("\tinstall-manual %s", manual))
|
||||
}
|
||||
|
||||
// install-desktop команды
|
||||
for _, desktop := range r.DesktopFiles {
|
||||
commands = append(commands, fmt.Sprintf("\tinstall-desktop %s", desktop))
|
||||
}
|
||||
|
||||
// install-systemd команды
|
||||
for _, service := range r.ServiceFiles {
|
||||
if strings.Contains(service, "user") {
|
||||
commands = append(commands, fmt.Sprintf("\tinstall-systemd-user %s", service))
|
||||
} else {
|
||||
commands = append(commands, fmt.Sprintf("\tinstall-systemd %s", service))
|
||||
}
|
||||
}
|
||||
|
||||
// install-completion команды
|
||||
for shell, file := range r.CompletionFiles {
|
||||
switch shell {
|
||||
case "bash":
|
||||
commands = append(commands, fmt.Sprintf("\tinstall-completion bash %s < %s", r.Name, file))
|
||||
case "zsh":
|
||||
commands = append(commands, fmt.Sprintf("\tinstall-completion zsh %s < %s", r.Name, file))
|
||||
case "fish":
|
||||
commands = append(commands, fmt.Sprintf("\t%s completion fish | install-completion fish %s", r.Name, r.Name))
|
||||
}
|
||||
}
|
||||
|
||||
if len(commands) == 0 {
|
||||
return "\t# TODO: Добавьте команды установки файлов"
|
||||
}
|
||||
|
||||
return strings.Join(commands, "\n")
|
||||
}
|
||||
|
||||
// fetchPKGBUILD загружает PKGBUILD файл для пакета
|
||||
func fetchPKGBUILD(packageBase string) (string, error) {
|
||||
// URL для raw PKGBUILD
|
||||
pkgbuildURL := fmt.Sprintf("https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=%s", packageBase)
|
||||
|
||||
res, err := http.Get(pkgbuildURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to fetch PKGBUILD: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return "", fmt.Errorf("failed to fetch PKGBUILD: status %s", res.Status)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read PKGBUILD: %w", err)
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// parseSources извлекает источники из PKGBUILD
|
||||
func parseSources(pkgbuild string) []string {
|
||||
var sources []string
|
||||
|
||||
// Регулярное выражение для поиска массива source
|
||||
// Поддерживает как однострочные, так и многострочные определения
|
||||
sourceRegex := regexp.MustCompile(`(?ms)source=\((.*?)\)`)
|
||||
matches := sourceRegex.FindStringSubmatch(pkgbuild)
|
||||
|
||||
if len(matches) > 1 {
|
||||
// Извлекаем содержимое массива source
|
||||
sourceContent := matches[1]
|
||||
|
||||
// Разбираем элементы массива
|
||||
// Учитываем кавычки и переносы строк
|
||||
elemRegex := regexp.MustCompile(`['"]([^'"]+)['"]`)
|
||||
elements := elemRegex.FindAllStringSubmatch(sourceContent, -1)
|
||||
|
||||
for _, elem := range elements {
|
||||
if len(elem) > 1 {
|
||||
source := elem[1]
|
||||
// Заменяем переменные версии
|
||||
source = strings.ReplaceAll(source, "$pkgver", "${version}")
|
||||
source = strings.ReplaceAll(source, "${pkgver}", "${version}")
|
||||
source = strings.ReplaceAll(source, "$pkgname", "${name}")
|
||||
source = strings.ReplaceAll(source, "${pkgname}", "${name}")
|
||||
// Обрабатываем другие переменные (упрощенно)
|
||||
source = strings.ReplaceAll(source, "$_commit", "${_commit}")
|
||||
sources = append(sources, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Если источники не найдены в source=(), проверяем source_x86_64 и другие архитектуры
|
||||
if len(sources) == 0 {
|
||||
archSourceRegex := regexp.MustCompile(`(?ms)source_(?:x86_64|aarch64)=\((.*?)\)`)
|
||||
matches = archSourceRegex.FindStringSubmatch(pkgbuild)
|
||||
if len(matches) > 1 {
|
||||
sourceContent := matches[1]
|
||||
elemRegex := regexp.MustCompile(`['"]([^'"]+)['"]`)
|
||||
elements := elemRegex.FindAllStringSubmatch(sourceContent, -1)
|
||||
|
||||
for _, elem := range elements {
|
||||
if len(elem) > 1 {
|
||||
source := elem[1]
|
||||
source = strings.ReplaceAll(source, "$pkgver", "${version}")
|
||||
source = strings.ReplaceAll(source, "${pkgver}", "${version}")
|
||||
source = strings.ReplaceAll(source, "$pkgname", "${name}")
|
||||
source = strings.ReplaceAll(source, "${pkgname}", "${name}")
|
||||
sources = append(sources, source)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sources
|
||||
}
|
||||
|
||||
// parseChecksums извлекает контрольные суммы из PKGBUILD
|
||||
func parseChecksums(pkgbuild string) []string {
|
||||
var checksums []string
|
||||
|
||||
// Пробуем разные типы контрольных сумм
|
||||
for _, hashType := range []string{"sha256sums", "sha512sums", "sha1sums", "md5sums", "b2sums"} {
|
||||
regex := regexp.MustCompile(fmt.Sprintf(`(?ms)%s=\((.*?)\)`, hashType))
|
||||
matches := regex.FindStringSubmatch(pkgbuild)
|
||||
|
||||
if len(matches) > 1 {
|
||||
content := matches[1]
|
||||
elemRegex := regexp.MustCompile(`['"]([^'"]+)['"]`)
|
||||
elements := elemRegex.FindAllStringSubmatch(content, -1)
|
||||
|
||||
for _, elem := range elements {
|
||||
if len(elem) > 1 {
|
||||
checksums = append(checksums, elem[1])
|
||||
}
|
||||
}
|
||||
|
||||
if len(checksums) > 0 {
|
||||
break // Используем первый найденный тип хешей
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return checksums
|
||||
}
|
||||
|
||||
// parseFunctions извлекает функции build(), package() и prepare() из PKGBUILD
|
||||
func parseFunctions(pkgbuild string) (buildFunc, packageFunc, prepareFunc string) {
|
||||
// Извлекаем функцию build()
|
||||
buildRegex := regexp.MustCompile(`(?ms)^build\(\)\s*\{(.*?)^\}`)
|
||||
if matches := buildRegex.FindStringSubmatch(pkgbuild); len(matches) > 1 {
|
||||
buildFunc = strings.TrimSpace(matches[1])
|
||||
}
|
||||
|
||||
// Извлекаем функцию package()
|
||||
packageRegex := regexp.MustCompile(`(?ms)^package\(\)\s*\{(.*?)^\}`)
|
||||
if matches := packageRegex.FindStringSubmatch(pkgbuild); len(matches) > 1 {
|
||||
packageFunc = strings.TrimSpace(matches[1])
|
||||
}
|
||||
|
||||
// Извлекаем функцию prepare()
|
||||
prepareRegex := regexp.MustCompile(`(?ms)^prepare\(\)\s*\{(.*?)^\}`)
|
||||
if matches := prepareRegex.FindStringSubmatch(pkgbuild); len(matches) > 1 {
|
||||
prepareFunc = strings.TrimSpace(matches[1])
|
||||
}
|
||||
|
||||
return buildFunc, packageFunc, prepareFunc
|
||||
}
|
||||
|
||||
// detectInstallableFiles анализирует PKGBUILD и определяет файлы для install-* команд
|
||||
func detectInstallableFiles(pkg *aurResult, pkgbuild string) {
|
||||
// Инициализируем карту для файлов автодополнения
|
||||
pkg.CompletionFiles = make(map[string]string)
|
||||
|
||||
// Для простоты, добавляем стандартные файлы для типа пакета
|
||||
switch pkg.PackageType {
|
||||
case "go":
|
||||
pkg.BinaryFiles = append(pkg.BinaryFiles, "./"+pkg.Name)
|
||||
case "rust":
|
||||
pkg.BinaryFiles = append(pkg.BinaryFiles, "./target/release/"+pkg.Name)
|
||||
case "cpp", "meson":
|
||||
pkg.BinaryFiles = append(pkg.BinaryFiles, "./"+pkg.Name) // обычно в корне после сборки
|
||||
case "bin":
|
||||
pkg.BinaryFiles = append(pkg.BinaryFiles, "./"+pkg.Name)
|
||||
default:
|
||||
if pkg.PackageType != "python" && pkg.PackageType != "nodejs" {
|
||||
pkg.BinaryFiles = append(pkg.BinaryFiles, "./"+pkg.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Ищем лицензионные файлы для install-license с более точными паттернами
|
||||
licenseRegex := regexp.MustCompile(`(?i)\b(LICENSE|COPYING|COPYRIGHT|LICENCE)(?:\.[a-zA-Z0-9]+)?\b`)
|
||||
licenseMatches := licenseRegex.FindAllString(pkgbuild, -1)
|
||||
for _, match := range licenseMatches {
|
||||
// Фильтруем только реальные файлы лицензий
|
||||
if strings.Contains(strings.ToLower(match), "license") ||
|
||||
strings.Contains(strings.ToLower(match), "copying") ||
|
||||
strings.Contains(strings.ToLower(match), "copyright") {
|
||||
if !contains(pkg.LicenseFiles, "./"+match) {
|
||||
pkg.LicenseFiles = append(pkg.LicenseFiles, "./"+match)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Если не найдены лицензионные файлы, добавляем стандартные
|
||||
if len(pkg.LicenseFiles) == 0 {
|
||||
pkg.LicenseFiles = append(pkg.LicenseFiles, "LICENSE")
|
||||
}
|
||||
|
||||
// Ищем man страницы для install-manual с более точными паттернами
|
||||
manRegex := regexp.MustCompile(`\b\w+\.(?:1|2|3|4|5|6|7|8)(?:\.gz)?\b`)
|
||||
manMatches := manRegex.FindAllString(pkgbuild, -1)
|
||||
for _, match := range manMatches {
|
||||
// Проверяем, что это не переменная или часть кода
|
||||
if !strings.Contains(match, "$") && !strings.Contains(match, "{") {
|
||||
if !contains(pkg.ManualFiles, "./"+match) {
|
||||
pkg.ManualFiles = append(pkg.ManualFiles, "./"+match)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ищем desktop файлы для install-desktop
|
||||
desktopRegex := regexp.MustCompile(`[^/\s]*\.desktop`)
|
||||
desktopMatches := desktopRegex.FindAllString(pkgbuild, -1)
|
||||
for _, match := range desktopMatches {
|
||||
if !contains(pkg.DesktopFiles, "./"+match) {
|
||||
pkg.DesktopFiles = append(pkg.DesktopFiles, "./"+match)
|
||||
}
|
||||
}
|
||||
|
||||
// Ищем systemd сервисы для install-systemd
|
||||
serviceRegex := regexp.MustCompile(`[^/\s]*\.service`)
|
||||
serviceMatches := serviceRegex.FindAllString(pkgbuild, -1)
|
||||
for _, match := range serviceMatches {
|
||||
if !contains(pkg.ServiceFiles, "./"+match) {
|
||||
pkg.ServiceFiles = append(pkg.ServiceFiles, "./"+match)
|
||||
}
|
||||
}
|
||||
|
||||
// Ищем файлы автодополнения
|
||||
completionPatterns := map[string]string{
|
||||
"bash": `completions?/.*\.bash|bash-completion`,
|
||||
"zsh": `completions?/.*\.zsh|zsh.*completion`,
|
||||
"fish": `completions?/.*\.fish|fish.*completion`,
|
||||
}
|
||||
|
||||
for shell, pattern := range completionPatterns {
|
||||
regex := regexp.MustCompile(fmt.Sprintf(`(?i)%s`, pattern))
|
||||
matches := regex.FindAllString(pkgbuild, -1)
|
||||
if len(matches) > 0 {
|
||||
pkg.CompletionFiles[shell] = matches[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// contains проверяет, содержит ли слайс строк указанную строку
|
||||
func contains(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// detectPackageType определяет тип пакета на основе имени, зависимостей и источников
|
||||
func detectPackageType(pkg *aurResult, pkgbuild string) {
|
||||
name := strings.ToLower(pkg.Name)
|
||||
|
||||
// Определяем тип на основе имени пакета
|
||||
switch {
|
||||
case strings.HasPrefix(name, "python") || strings.HasPrefix(name, "python3-"):
|
||||
pkg.PackageType = "python"
|
||||
case strings.Contains(name, "nodejs") || strings.Contains(name, "node-"):
|
||||
pkg.PackageType = "nodejs"
|
||||
case strings.HasSuffix(name, "-bin"):
|
||||
pkg.PackageType = "bin"
|
||||
case strings.HasSuffix(name, "-git"):
|
||||
pkg.PackageType = "git"
|
||||
pkg.HasVersion = true // Git пакеты обычно имеют функцию version()
|
||||
case strings.Contains(name, "rust") || hasRustSources(pkg.Sources):
|
||||
pkg.PackageType = "rust"
|
||||
case strings.Contains(name, "go-") || hasGoSources(pkg.Sources):
|
||||
pkg.PackageType = "go"
|
||||
case strings.Contains(name, "-rust") || strings.Contains(name, "paru") || strings.Contains(name, "cargo-"):
|
||||
pkg.PackageType = "rust"
|
||||
default:
|
||||
// Определяем по зависимостям сборки
|
||||
for _, dep := range pkg.MakeDepends {
|
||||
depLower := strings.ToLower(dep)
|
||||
switch {
|
||||
case strings.Contains(depLower, "meson") || strings.Contains(depLower, "ninja"):
|
||||
pkg.PackageType = "meson"
|
||||
case strings.Contains(depLower, "cmake") || strings.Contains(depLower, "gcc") || strings.Contains(depLower, "clang"):
|
||||
pkg.PackageType = "cpp"
|
||||
case strings.Contains(depLower, "python"):
|
||||
pkg.PackageType = "python"
|
||||
case strings.Contains(depLower, "go"):
|
||||
pkg.PackageType = "go"
|
||||
case strings.Contains(depLower, "rust") || strings.Contains(depLower, "cargo"):
|
||||
pkg.PackageType = "rust"
|
||||
case strings.Contains(depLower, "npm") || strings.Contains(depLower, "nodejs"):
|
||||
pkg.PackageType = "nodejs"
|
||||
}
|
||||
if pkg.PackageType != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Определяем архитектуры на основе типа пакета
|
||||
if pkg.PackageType == "bin" {
|
||||
pkg.Architectures = []string{"amd64"} // Бинарные пакеты обычно специфичны для архитектуры
|
||||
} else {
|
||||
pkg.Architectures = []string{"all"} // Исходный код собирается для любой архитектуры
|
||||
}
|
||||
|
||||
// Определяем наличие desktop файлов
|
||||
pkg.HasDesktop = strings.Contains(pkgbuild, ".desktop") ||
|
||||
strings.Contains(pkgbuild, "install-desktop") ||
|
||||
strings.Contains(pkgbuild, "xdg-desktop")
|
||||
|
||||
// Определяем наличие systemd сервисов
|
||||
pkg.HasSystemd = strings.Contains(pkgbuild, ".service") ||
|
||||
strings.Contains(pkgbuild, "systemctl") ||
|
||||
strings.Contains(pkgbuild, "install-systemd")
|
||||
|
||||
// Определяем наличие функции version() для -git пакетов
|
||||
pkg.HasVersion = strings.Contains(pkgbuild, "pkgver()") ||
|
||||
(strings.HasSuffix(name, "-git") && strings.Contains(pkgbuild, "git describe"))
|
||||
|
||||
// Определяем наличие патчей
|
||||
pkg.HasPatches = strings.Contains(pkgbuild, "patch ") ||
|
||||
strings.Contains(pkgbuild, ".patch") ||
|
||||
strings.Contains(pkgbuild, ".diff")
|
||||
|
||||
// Определяем дополнительные скрипты
|
||||
if strings.Contains(pkgbuild, "post_install") {
|
||||
pkg.HasScripts = append(pkg.HasScripts, "postinstall")
|
||||
}
|
||||
if strings.Contains(pkgbuild, "pre_remove") || strings.Contains(pkgbuild, "post_remove") {
|
||||
pkg.HasScripts = append(pkg.HasScripts, "postremove")
|
||||
}
|
||||
}
|
||||
|
||||
// hasRustSources проверяет, содержат ли источники Rust проекты
|
||||
func hasRustSources(sources []string) bool {
|
||||
for _, src := range sources {
|
||||
if strings.Contains(src, "crates.io") || strings.Contains(src, "Cargo.toml") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasGoSources проверяет, содержат ли источники Go проекты
|
||||
func hasGoSources(sources []string) bool {
|
||||
for _, src := range sources {
|
||||
if strings.Contains(src, "github.com") && strings.Contains(src, "/go") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AUR генерирует шаблон alr.sh на основе пакета из AUR
|
||||
func AUR(w io.Writer, opts AUROptions) error {
|
||||
// Создаем шаблон с функциями
|
||||
tmpl, err := template.New("aur").
|
||||
Funcs(funcs).
|
||||
Parse(aurTmpl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Формируем URL запроса к AUR API
|
||||
apiURL := "https://aur.archlinux.org/rpc/v5/info"
|
||||
params := url.Values{}
|
||||
params.Add("arg[]", opts.Name)
|
||||
fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode())
|
||||
|
||||
// Выполняем запрос к AUR API
|
||||
res, err := http.Get(fullURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch AUR package info: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
return fmt.Errorf("AUR API returned status: %s", res.Status)
|
||||
}
|
||||
|
||||
// Декодируем ответ
|
||||
var resp aurAPIResponse
|
||||
err = json.NewDecoder(res.Body).Decode(&resp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode AUR response: %w", err)
|
||||
}
|
||||
|
||||
// Проверяем наличие ошибки в ответе
|
||||
if resp.Error != "" {
|
||||
return fmt.Errorf("AUR API error: %s", resp.Error)
|
||||
}
|
||||
|
||||
// Проверяем, что пакет найден
|
||||
if resp.ResultCount == 0 {
|
||||
return fmt.Errorf("package '%s' not found in AUR", opts.Name)
|
||||
}
|
||||
|
||||
// Берем первый результат
|
||||
pkg := resp.Results[0]
|
||||
|
||||
// Если указана версия, проверяем соответствие
|
||||
if opts.Version != "" && pkg.Version != opts.Version {
|
||||
// Предупреждаем, но продолжаем с актуальной версией из AUR
|
||||
fmt.Fprintf(w, "# WARNING: Requested version %s, but AUR has %s\n", opts.Version, pkg.Version)
|
||||
}
|
||||
|
||||
// Загружаем PKGBUILD для получения источников
|
||||
pkgbuild, err := fetchPKGBUILD(pkg.PackageBase)
|
||||
if err != nil {
|
||||
// Если не удалось загрузить PKGBUILD, используем fallback на AUR репозиторий
|
||||
fmt.Fprintf(w, "# WARNING: Could not fetch PKGBUILD: %v\n", err)
|
||||
fmt.Fprintf(w, "# Using AUR repository as source\n")
|
||||
pkg.Sources = []string{fmt.Sprintf("%s::git+%s", pkg.Name, pkg.GitURL())}
|
||||
pkg.Checksums = []string{"SKIP"}
|
||||
} else {
|
||||
// Извлекаем источники из PKGBUILD
|
||||
pkg.Sources = parseSources(pkgbuild)
|
||||
pkg.Checksums = parseChecksums(pkgbuild)
|
||||
pkg.BuildFunc, pkg.PackageFunc, pkg.PrepareFunc = parseFunctions(pkgbuild)
|
||||
|
||||
// Определяем тип пакета
|
||||
detectPackageType(&pkg, pkgbuild)
|
||||
|
||||
// Определяем файлы для install-* команд
|
||||
detectInstallableFiles(&pkg, pkgbuild)
|
||||
|
||||
// Если источники не найдены, используем fallback
|
||||
if len(pkg.Sources) == 0 {
|
||||
fmt.Fprintf(w, "# WARNING: No sources found in PKGBUILD\n")
|
||||
fmt.Fprintf(w, "# Using AUR repository as source\n")
|
||||
pkg.Sources = []string{fmt.Sprintf("%s::git+%s", pkg.Name, pkg.GitURL())}
|
||||
pkg.Checksums = []string{"SKIP"}
|
||||
}
|
||||
}
|
||||
|
||||
// Выполняем шаблон
|
||||
return tmpl.Execute(w, pkg)
|
||||
}
|
133
internal/gen/tmpls/aur.tmpl.sh
Normal file
133
internal/gen/tmpls/aur.tmpl.sh
Normal file
@@ -0,0 +1,133 @@
|
||||
# This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
|
||||
# It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
|
||||
#
|
||||
# ALR - Any Linux Repository
|
||||
# Copyright (C) 2025 The ALR Authors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Generated from AUR package: {{.Name}}
|
||||
# Package type: {{.PackageType}}
|
||||
# AUR votes: {{.NumVotes}} | Popularity: {{printf "%.2f" .Popularity}}
|
||||
# Original maintainer: {{.Maintainer}}
|
||||
# Adapted for ALR by automation
|
||||
|
||||
name='{{.Name}}'
|
||||
version='{{.Version}}'
|
||||
release='1'
|
||||
desc='{{.Description}}'
|
||||
{{if ne .Description ""}}desc_ru='{{.Description}}'{{end}}
|
||||
homepage='{{.URL}}'
|
||||
maintainer="Евгений Храмов <xpamych@yandex.ru> (imported from AUR)"
|
||||
{{if ne .Description ""}}maintainer_ru="Евгений Храмов <xpamych@yandex.ru> (импортирован из AUR)"{{end}}
|
||||
architectures=({{.ArchitecturesString}})
|
||||
license=({{.LicenseString}})
|
||||
{{if .Provides}}provides=({{range .Provides}}'{{.}}' {{end}}){{end}}
|
||||
{{if .Conflicts}}conflicts=({{range .Conflicts}}'{{.}}' {{end}}){{end}}
|
||||
{{if .Replaces}}replaces=({{range .Replaces}}'{{.}}' {{end}}){{end}}
|
||||
|
||||
# Базовые зависимости
|
||||
{{if .DependsString}}deps=({{.DependsString}}){{else}}deps=(){{end}}
|
||||
{{if .MakeDependsString}}build_deps=({{.MakeDependsString}}){{else}}build_deps=(){{end}}
|
||||
|
||||
# Зависимости для конкретных дистрибутивов (адаптируйте под нужды пакета)
|
||||
{{if .DependsString}}deps_arch=({{.DependsString}})
|
||||
deps_debian=({{.DependsString}})
|
||||
deps_altlinux=({{.DependsString}})
|
||||
deps_alpine=({{.DependsString}}){{end}}
|
||||
|
||||
{{if and .MakeDependsString (ne .PackageType "bin")}}# Зависимости сборки для конкретных дистрибутивов
|
||||
build_deps_arch=({{.MakeDependsString}})
|
||||
build_deps_debian=({{.MakeDependsString}})
|
||||
build_deps_altlinux=({{.MakeDependsString}})
|
||||
build_deps_alpine=({{.MakeDependsString}}){{end}}
|
||||
|
||||
{{if .OptDependsString}}# Опциональные зависимости
|
||||
opt_deps=(
|
||||
{{.OptDependsString}}
|
||||
){{end}}
|
||||
|
||||
# Источники из PKGBUILD
|
||||
sources=({{range .Sources}}"{{.}}" {{end}})
|
||||
checksums=({{range .Checksums}}'{{.}}' {{end}})
|
||||
|
||||
{{if .HasVersion}}# Функция версии для Git-пакетов
|
||||
version() {
|
||||
cd "$srcdir/{{.Name}}"
|
||||
git-version
|
||||
}
|
||||
{{end}}
|
||||
|
||||
{{if .ScriptsString}}# Дополнительные скрипты
|
||||
scripts=(
|
||||
{{.ScriptsString}}
|
||||
){{end}}
|
||||
|
||||
{{if or .PrepareFunc .HasPatches}}prepare() {
|
||||
cd "$srcdir"{{if .PrepareFunc}}
|
||||
# Из PKGBUILD:
|
||||
{{.PrepareFunc}}{{else}}
|
||||
# Применение патчей и подготовка исходников
|
||||
# Раскомментируйте и адаптируйте при необходимости:
|
||||
# patch -p1 < "${scriptdir}/fix.patch"{{end}}
|
||||
}{{else}}# prepare() {
|
||||
# cd "$srcdir"
|
||||
# # Применение патчей и подготовка исходников при необходимости
|
||||
# # patch -p1 < "${scriptdir}/fix.patch"
|
||||
# }{{end}}
|
||||
|
||||
{{if ne .PackageType "bin"}}build() {
|
||||
cd "$srcdir"{{if .BuildFunc}}
|
||||
# Из PKGBUILD:
|
||||
{{.BuildFunc}}{{else}}
|
||||
|
||||
# TODO: Адаптируйте команды сборки под конкретный проект ({{.PackageType}})
|
||||
{{if eq .PackageType "meson"}}# Для Meson проектов:
|
||||
meson setup build --prefix=/usr --buildtype=release
|
||||
ninja -C build -j $(nproc){{else if eq .PackageType "cpp"}}# Для C/C++ проектов:
|
||||
mkdir -p build && cd build
|
||||
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
|
||||
make -j$(nproc){{else if eq .PackageType "go"}}# Для Go проектов:
|
||||
go build -buildmode=pie -trimpath -ldflags "-s -w" -o {{.Name}}{{else if eq .PackageType "python"}}# Для Python проектов:
|
||||
python -m build --wheel --no-isolation{{else if eq .PackageType "nodejs"}}# Для Node.js проектов:
|
||||
npm ci --production
|
||||
npm run build{{else if eq .PackageType "rust"}}# Для Rust проектов:
|
||||
cargo build --release --locked{{else if eq .PackageType "git"}}# Для Git проектов (обычно исходный код):
|
||||
make -j$(nproc){{else}}# Стандартная сборка:
|
||||
make -j$(nproc){{end}}{{end}}
|
||||
}{{else}}# Бинарный пакет - сборка не требуется{{end}}
|
||||
|
||||
package() {
|
||||
cd "$srcdir"{{if .PackageFunc}}
|
||||
# Из PKGBUILD (адаптировано для ALR):
|
||||
{{.PackageFunc}}
|
||||
|
||||
# Автоматически сгенерированные команды установки:
|
||||
{{.GenerateInstallCommands}}{{else}}
|
||||
|
||||
# TODO: Адаптируйте установку файлов под конкретный проект {{.Name}}
|
||||
{{if eq .PackageType "meson"}}# Для Meson проектов:
|
||||
meson install -C build --destdir="$pkgdir"{{else if eq .PackageType "cpp"}}# Для C/C++ проектов:
|
||||
cd build
|
||||
make DESTDIR="$pkgdir" install{{else if eq .PackageType "go"}}# Для Go проектов:
|
||||
# Исполняемый файл уже собран в корне{{else if eq .PackageType "python"}}# Для Python проектов:
|
||||
pip install --root="$pkgdir/" . --no-deps --disable-pip-version-check{{else if eq .PackageType "nodejs"}}# Для Node.js проектов:
|
||||
npm install -g --prefix="$pkgdir/usr" .{{else if eq .PackageType "rust"}}# Для Rust проектов:
|
||||
# Исполняемый файл в target/release/{{else if eq .PackageType "bin"}}# Бинарный пакет:
|
||||
# Файлы уже распакованы{{else}}# Стандартная установка:
|
||||
make DESTDIR="$pkgdir" install{{end}}
|
||||
|
||||
# Автоматически сгенерированные команды установки:
|
||||
{{.GenerateInstallCommands}}{{end}}
|
||||
}
|
@@ -16,14 +16,30 @@
|
||||
|
||||
package manager
|
||||
|
||||
import "os/exec"
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
type CommonPackageManager struct {
|
||||
noConfirmArg string
|
||||
}
|
||||
|
||||
func (m *CommonPackageManager) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
|
||||
cmd := exec.Command(mgrCmd)
|
||||
var cmd *exec.Cmd
|
||||
|
||||
// Проверяем, нужно ли повышение привилегий
|
||||
isRoot := os.Geteuid() == 0
|
||||
isCI := os.Getenv("CI") == "true"
|
||||
|
||||
if !isRoot && !isCI {
|
||||
// Если не root и не в CI, используем sudo
|
||||
cmd = exec.Command("sudo", mgrCmd)
|
||||
} else {
|
||||
// Если root или в CI, запускаем напрямую
|
||||
cmd = exec.Command(mgrCmd)
|
||||
}
|
||||
|
||||
cmd.Args = append(cmd.Args, opts.Args...)
|
||||
cmd.Args = append(cmd.Args, args...)
|
||||
|
||||
|
@@ -60,6 +60,13 @@ func (rs *Repos) FindPkgs(ctx context.Context, pkgs []string) (map[string][]alrs
|
||||
return nil, nil, fmt.Errorf("FindPkgs: get by provides: %w", err)
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
result, err = rs.db.GetPkgs(ctx, "basepkg_name = ?", pkgName)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("FindPkgs: get by basepkg_name: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(result) == 0 {
|
||||
result, err = rs.db.GetPkgs(ctx, "name LIKE ?", pkgName)
|
||||
}
|
||||
|
106
internal/stats/tracker.go
Normal file
106
internal/stats/tracker.go
Normal file
@@ -0,0 +1,106 @@
|
||||
// ALR - Any Linux Repository
|
||||
// Copyright (C) 2025 The ALR Authors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package stats
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type InstallationData struct {
|
||||
PackageName string `json:"packageName"`
|
||||
Version string `json:"version,omitempty"`
|
||||
InstallType string `json:"installType"` // "install" or "upgrade"
|
||||
UserAgent string `json:"userAgent"`
|
||||
Fingerprint string `json:"fingerprint,omitempty"`
|
||||
}
|
||||
|
||||
var (
|
||||
apiEndpoints = []string{
|
||||
"https://alr.plemya-x.ru/api/packages/track-install",
|
||||
"http://localhost:3001/api/packages/track-install",
|
||||
}
|
||||
userAgent = "ALR-CLI/1.0"
|
||||
)
|
||||
|
||||
func generateFingerprint(packageName string) string {
|
||||
hostname, _ := os.Hostname()
|
||||
data := fmt.Sprintf("%s_%s_%s", hostname, packageName, time.Now().Format("2006-01-02"))
|
||||
hash := sha256.Sum256([]byte(data))
|
||||
return hex.EncodeToString(hash[:])
|
||||
}
|
||||
|
||||
// TrackInstallation отправляет статистику установки пакета
|
||||
func TrackInstallation(ctx context.Context, packageName string, installType string) {
|
||||
// Запускаем в отдельной горутине, чтобы не блокировать основной процесс
|
||||
go func() {
|
||||
data := InstallationData{
|
||||
PackageName: packageName,
|
||||
InstallType: installType,
|
||||
UserAgent: userAgent,
|
||||
Fingerprint: generateFingerprint(packageName),
|
||||
}
|
||||
|
||||
jsonData, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return // Тихо игнорируем ошибки - статистика не критична
|
||||
}
|
||||
|
||||
// Пробуем отправить запрос к разным endpoint-ам
|
||||
for _, endpoint := range apiEndpoints {
|
||||
if sendRequest(endpoint, jsonData) {
|
||||
return // Если хотя бы один запрос прошёл успешно, выходим
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func sendRequest(endpoint string, data []byte) bool {
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", endpoint, bytes.NewBuffer(data))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("User-Agent", userAgent)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return resp.StatusCode >= 200 && resp.StatusCode < 300
|
||||
}
|
||||
|
||||
// ShouldTrackPackage проверяет, нужно ли отслеживать установку этого пакета
|
||||
func ShouldTrackPackage(packageName string) bool {
|
||||
// Отслеживаем только alr-bin
|
||||
return strings.Contains(packageName, "alr-bin")
|
||||
}
|
@@ -17,12 +17,9 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"strconv"
|
||||
"syscall"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
"github.com/urfave/cli/v2"
|
||||
@@ -32,115 +29,23 @@ import (
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
|
||||
)
|
||||
|
||||
func GetUidGidAlrUserString() (string, string, error) {
|
||||
u, err := user.Lookup("alr")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return u.Uid, u.Gid, nil
|
||||
}
|
||||
|
||||
func GetUidGidAlrUser() (int, int, error) {
|
||||
strUid, strGid, err := GetUidGidAlrUserString()
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
uid, err := strconv.Atoi(strUid)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
gid, err := strconv.Atoi(strGid)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
|
||||
return uid, gid, nil
|
||||
}
|
||||
|
||||
func DropCapsToAlrUser() error {
|
||||
uid, gid, err := GetUidGidAlrUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = syscall.Setgid(gid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = syscall.Setuid(uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return EnsureIsAlrUser()
|
||||
}
|
||||
|
||||
func ExitIfCantDropGidToAlr() cli.ExitCoder {
|
||||
_, gid, err := GetUidGidAlrUser()
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit("cannot get gid alr", err)
|
||||
}
|
||||
err = syscall.Setgid(gid)
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit("cannot get setgid alr", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExitIfCantDropCapsToAlrUser attempts to drop capabilities to the already
|
||||
// running user. Returns a cli.ExitCoder with an error if the operation fails.
|
||||
// See also [ExitIfCantDropCapsToAlrUserNoPrivs] for a version that also applies
|
||||
// no-new-privs.
|
||||
func ExitIfCantDropCapsToAlrUser() cli.ExitCoder {
|
||||
err := DropCapsToAlrUser()
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit(gotext.Get("Error on dropping capabilities"), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ExitIfCantSetNoNewPrivs() cli.ExitCoder {
|
||||
if err := NoNewPrivs(); err != nil {
|
||||
return cliutils.FormatCliExit("error on NoNewPrivs", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExitIfCantDropCapsToAlrUserNoPrivs combines [ExitIfCantDropCapsToAlrUser] with [ExitIfCantSetNoNewPrivs]
|
||||
func ExitIfCantDropCapsToAlrUserNoPrivs() cli.ExitCoder {
|
||||
if err := ExitIfCantDropCapsToAlrUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ExitIfCantSetNoNewPrivs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsNotRoot проверяет, что текущий пользователь не является root
|
||||
func IsNotRoot() bool {
|
||||
return os.Getuid() != 0
|
||||
}
|
||||
|
||||
func EnsureIsAlrUser() error {
|
||||
uid, gid, err := GetUidGidAlrUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
newUid := syscall.Getuid()
|
||||
if newUid != uid {
|
||||
return errors.New("uid don't matches requested")
|
||||
}
|
||||
newGid := syscall.Getgid()
|
||||
if newGid != gid {
|
||||
return errors.New("gid don't matches requested")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnuseIsPrivilegedGroupMember проверяет, что пользователь является членом привилегированной группы (wheel)
|
||||
func EnuseIsPrivilegedGroupMember() error {
|
||||
// В CI пропускаем проверку группы wheel
|
||||
if os.Getenv("CI") == "true" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Если пользователь root, пропускаем проверку
|
||||
if os.Geteuid() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
return func(ctx *cli.Context) error {
|
||||
deps, err := appbuilder.
|
||||
|
@@ -16,8 +16,78 @@
|
||||
|
||||
package utils
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func NoNewPrivs() error {
|
||||
return unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
|
||||
}
|
||||
|
||||
// EnsureTempDirWithRootOwner создает каталог в /tmp/alr с правами для группы 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)
|
||||
}
|
||||
|
4
list.go
4
list.go
@@ -35,7 +35,6 @@ import (
|
||||
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
|
||||
)
|
||||
|
||||
@@ -60,9 +59,6 @@ func ListCmd() *cli.Command {
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := c.Context
|
||||
|
||||
|
1
main.go
1
main.go
@@ -87,7 +87,6 @@ func GetApp() *cli.App {
|
||||
// Internal commands
|
||||
InternalBuildCmd(),
|
||||
InternalInstallCmd(),
|
||||
InternalMountCmd(),
|
||||
InternalReposCmd(),
|
||||
},
|
||||
Before: func(c *cli.Context) error {
|
||||
|
42
pkg/dl/dl.go
42
pkg/dl/dl.go
@@ -280,14 +280,14 @@ func handleCache(cacheDir, dest, name string, t Type) (bool, error) {
|
||||
cd.Close()
|
||||
|
||||
if slices.Contains(names, name) {
|
||||
err = os.Link(filepath.Join(cacheDir, name), dest)
|
||||
err = linkOrCopy(filepath.Join(cacheDir, name), dest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
case TypeDir:
|
||||
err := linkDir(cacheDir, dest)
|
||||
err := linkOrCopyDir(cacheDir, dest)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
@@ -296,8 +296,40 @@ func handleCache(cacheDir, dest, name string, t Type) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Функция linkDir рекурсивно создает жесткие ссылки для файлов из каталога src в каталог dest
|
||||
func linkDir(src, dest string) error {
|
||||
// linkOrCopy пытается создать жесткую ссылку, а если не получается - копирует файл
|
||||
func linkOrCopy(src, dest string) error {
|
||||
err := os.Link(src, dest)
|
||||
if err != nil {
|
||||
// Если не удалось создать ссылку, копируем файл
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
destFile, err := os.Create(dest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
_, err = io.Copy(destFile, srcFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Копируем права доступа
|
||||
srcInfo, err := srcFile.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chmod(dest, srcInfo.Mode())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// linkOrCopyDir рекурсивно создает жесткие ссылки или копирует файлы из каталога src в каталог dest
|
||||
func linkOrCopyDir(src, dest string) error {
|
||||
return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -317,7 +349,7 @@ func linkDir(src, dest string) error {
|
||||
return os.MkdirAll(newPath, info.Mode())
|
||||
}
|
||||
|
||||
return os.Link(path, newPath)
|
||||
return linkOrCopy(path, newPath)
|
||||
})
|
||||
}
|
||||
|
||||
|
@@ -61,7 +61,8 @@ func (dc *DownloadCache) New(ctx context.Context, id string) (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
err = os.MkdirAll(itemPath, 0o755)
|
||||
// Создаем директорию с правильными правами (различается для prod и тестов)
|
||||
err = createDir(itemPath, 0o2775)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
37
pkg/dlcache/dlcache_prod.go
Normal file
37
pkg/dlcache/dlcache_prod.go
Normal file
@@ -0,0 +1,37 @@
|
||||
//go:build !test
|
||||
|
||||
// ALR - Any Linux Repository
|
||||
// Copyright (C) 2025 The ALR Authors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package dlcache
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
|
||||
)
|
||||
|
||||
// createDir создает директорию с правильными правами для production
|
||||
func createDir(itemPath string, mode os.FileMode) error {
|
||||
// Используем специальную функцию для создания каталогов с setgid битом только для /tmp/alr
|
||||
// В остальных случаях используем обычное создание директории
|
||||
if strings.HasPrefix(itemPath, "/tmp/alr") {
|
||||
return utils.EnsureTempDirWithRootOwner(itemPath, mode)
|
||||
} else {
|
||||
return os.MkdirAll(itemPath, mode)
|
||||
}
|
||||
}
|
@@ -45,7 +45,7 @@ func (c *TestALRConfig) GetPaths() *config.Paths {
|
||||
func prepare(t *testing.T) *TestALRConfig {
|
||||
t.Helper()
|
||||
|
||||
dir, err := os.MkdirTemp("/tmp", "alr-dlcache-test.*")
|
||||
dir, err := os.MkdirTemp("", "alr-dlcache-test.*")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -57,7 +57,7 @@ func prepare(t *testing.T) *TestALRConfig {
|
||||
|
||||
func cleanup(t *testing.T, cfg *TestALRConfig) {
|
||||
t.Helper()
|
||||
os.Remove(cfg.CacheDir)
|
||||
os.RemoveAll(cfg.CacheDir)
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
@@ -82,6 +82,12 @@ func TestNew(t *testing.T) {
|
||||
fi, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
t.Errorf("stat: expected no error, got %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
if fi == nil {
|
||||
t.Errorf("Expected file info to not be nil")
|
||||
return
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
|
28
pkg/dlcache/dlcache_test_impl.go
Normal file
28
pkg/dlcache/dlcache_test_impl.go
Normal file
@@ -0,0 +1,28 @@
|
||||
//go:build test
|
||||
|
||||
// ALR - Any Linux Repository
|
||||
// Copyright (C) 2025 The ALR Authors
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package dlcache
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// createDir создает директорию с обычными правами для тестирования
|
||||
func createDir(itemPath string, mode os.FileMode) error {
|
||||
return os.MkdirAll(itemPath, mode)
|
||||
}
|
@@ -21,13 +21,14 @@ package types
|
||||
|
||||
// Config represents the ALR configuration file
|
||||
type Config struct {
|
||||
RootCmd string `json:"rootCmd" koanf:"rootCmd"`
|
||||
UseRootCmd bool `json:"useRootCmd" koanf:"useRootCmd"`
|
||||
PagerStyle string `json:"pagerStyle" koanf:"pagerStyle"`
|
||||
IgnorePkgUpdates []string `json:"ignorePkgUpdates" koanf:"ignorePkgUpdates"`
|
||||
Repos []Repo `json:"repo" koanf:"repo"`
|
||||
AutoPull bool `json:"autoPull" koanf:"autoPull"`
|
||||
LogLevel string `json:"logLevel" koanf:"logLevel"`
|
||||
RootCmd string `json:"rootCmd" koanf:"rootCmd"`
|
||||
UseRootCmd bool `json:"useRootCmd" koanf:"useRootCmd"`
|
||||
PagerStyle string `json:"pagerStyle" koanf:"pagerStyle"`
|
||||
IgnorePkgUpdates []string `json:"ignorePkgUpdates" koanf:"ignorePkgUpdates"`
|
||||
Repos []Repo `json:"repo" koanf:"repo"`
|
||||
AutoPull bool `json:"autoPull" koanf:"autoPull"`
|
||||
LogLevel string `json:"logLevel" koanf:"logLevel"`
|
||||
UpdateSystemOnUpgrade bool `json:"updateSystemOnUpgrade" koanf:"updateSystemOnUpgrade"`
|
||||
}
|
||||
|
||||
// Repo represents a ALR repo within a configuration file
|
||||
|
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/urfave/cli/v2"
|
||||
|
||||
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 {
|
||||
@@ -30,9 +29,6 @@ func RefreshCmd() *cli.Command {
|
||||
Usage: gotext.Get("Pull all repositories that have changed"),
|
||||
Aliases: []string{"ref"},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := c.Context
|
||||
|
||||
|
3
repo.go
3
repo.go
@@ -114,9 +114,6 @@ func RemoveRepoCmd() *cli.Command {
|
||||
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
|
||||
}
|
||||
|
||||
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deps, err = appbuilder.
|
||||
New(ctx).
|
||||
|
37
scripts/fmt-precommit.sh
Executable file
37
scripts/fmt-precommit.sh
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
# ALR - Any Linux Repository
|
||||
# Copyright (C) 2025 The ALR Authors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
set -e
|
||||
|
||||
# Запускаем форматирование
|
||||
make fmt || true
|
||||
|
||||
# Проверяем какие файлы были изменены (только те, что отслеживаются git)
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=M | grep '\.go$' || true)
|
||||
|
||||
# Если файлы были изменены, добавляем их в git
|
||||
if [ ! -z "$CHANGED_FILES" ]; then
|
||||
echo "Formatting changed the following files:"
|
||||
echo "$CHANGED_FILES"
|
||||
# Добавляем только измененные файлы, которые уже отслеживаются
|
||||
echo "$CHANGED_FILES" | xargs -r git add
|
||||
echo "Files were formatted and staged"
|
||||
fi
|
||||
|
||||
echo "Formatting completed"
|
||||
# Всегда возвращаем успех
|
||||
exit 0
|
63
scripts/i18n-precommit.sh
Executable file
63
scripts/i18n-precommit.sh
Executable file
@@ -0,0 +1,63 @@
|
||||
#!/bin/bash
|
||||
# ALR - Any Linux Repository
|
||||
# Copyright (C) 2025 The ALR Authors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# Wrapper script for i18n that automatically stages changed files for pre-commit
|
||||
|
||||
set -e
|
||||
|
||||
# Сохраняем состояние файлов до выполнения i18n
|
||||
TRANSLATION_FILES=(
|
||||
"internal/translations/default.pot"
|
||||
"internal/translations/po/ru/default.po"
|
||||
"assets/i18n-ru-badge.svg"
|
||||
)
|
||||
|
||||
# Создаем временные файлы для сравнения
|
||||
TEMP_DIR=$(mktemp -d)
|
||||
for file in "${TRANSLATION_FILES[@]}"; do
|
||||
if [[ -f "$file" ]]; then
|
||||
cp "$file" "$TEMP_DIR/$(basename "$file")"
|
||||
fi
|
||||
done
|
||||
|
||||
# Выполняем обновление переводов
|
||||
make i18n
|
||||
|
||||
# Проверяем какие файлы изменились и добавляем их в staging area
|
||||
CHANGED_FILES=()
|
||||
for file in "${TRANSLATION_FILES[@]}"; do
|
||||
if [[ -f "$file" ]]; then
|
||||
if [[ ! -f "$TEMP_DIR/$(basename "$file")" ]] || ! cmp -s "$file" "$TEMP_DIR/$(basename "$file")"; then
|
||||
CHANGED_FILES+=("$file")
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Добавляем измененные файлы в git staging area
|
||||
if [[ ${#CHANGED_FILES[@]} -gt 0 ]]; then
|
||||
echo "Auto-staging changed translation files:"
|
||||
for file in "${CHANGED_FILES[@]}"; do
|
||||
echo " - $file"
|
||||
git add "$file"
|
||||
done
|
||||
fi
|
||||
|
||||
# Очищаем временные файлы
|
||||
rm -rf "$TEMP_DIR"
|
||||
|
||||
# Выход с кодом 0 (успех) даже если файлы были изменены
|
||||
exit 0
|
@@ -32,12 +32,20 @@ error() {
|
||||
|
||||
installPkg() {
|
||||
rootCmd=""
|
||||
if command -v doas &>/dev/null; then
|
||||
rootCmd="doas"
|
||||
elif command -v sudo &>/dev/null; then
|
||||
rootCmd="sudo"
|
||||
|
||||
# Проверяем, запущен ли скрипт от root
|
||||
if [ "$(id -u)" = "0" ]; then
|
||||
# Если root, не используем sudo/doas
|
||||
rootCmd=""
|
||||
else
|
||||
warn "Не обнаружена команда повышения привилегий (например, sudo, doas)"
|
||||
# Если не root, ищем команду повышения привилегий
|
||||
if command -v doas &>/dev/null; then
|
||||
rootCmd="doas"
|
||||
elif command -v sudo &>/dev/null; then
|
||||
rootCmd="sudo"
|
||||
else
|
||||
warn "Не обнаружена команда повышения привилегий (например, sudo, doas)"
|
||||
fi
|
||||
fi
|
||||
|
||||
case $1 in
|
||||
@@ -48,10 +56,46 @@ installPkg() {
|
||||
esac
|
||||
}
|
||||
|
||||
trackInstallation() {
|
||||
# Отправить статистику установки (не критично если не получится)
|
||||
if command -v curl &>/dev/null; then
|
||||
# Генерируем уникальный отпечаток на основе hostname и даты
|
||||
fingerprint=$(echo "$(hostname)_$(date +%Y-%m-%d)" | sha256sum 2>/dev/null | cut -d' ' -f1 || echo "$(hostname)_$(date +%Y-%m-%d)")
|
||||
|
||||
# Пробуем разные домены/порты для отправки статистики
|
||||
for api_url in "https://alr.plemya-x.ru/api/packages/track-install" "http://localhost:3001/api/packages/track-install"; do
|
||||
curl -s -m 5 -X POST "$api_url" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "User-Agent: ALR-InstallScript/1.0" \
|
||||
-d "{
|
||||
\"packageName\": \"alr-bin\",
|
||||
\"installType\": \"script\",
|
||||
\"userAgent\": \"ALR-InstallScript/1.0\",
|
||||
\"fingerprint\": \"$fingerprint\"
|
||||
}" >/dev/null 2>&1
|
||||
# Если один запрос удался, не пробуем остальные
|
||||
if [ $? -eq 0 ]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
if ! command -v curl &>/dev/null; then
|
||||
error "Этот скрипт требует команду curl. Пожалуйста, установите её и запустите снова."
|
||||
fi
|
||||
|
||||
# Определение архитектуры системы
|
||||
arch=$(uname -m)
|
||||
case $arch in
|
||||
x86_64) debArch="amd64"; rpmArch="x86_64" ;;
|
||||
aarch64) debArch="arm64"; rpmArch="aarch64" ;;
|
||||
armv7l) debArch="armhf"; rpmArch="armv7hl" ;;
|
||||
*) error "Неподдерживаемая архитектура: $arch" ;;
|
||||
esac
|
||||
|
||||
info "Обнаружена архитектура: $arch"
|
||||
|
||||
pkgFormat=""
|
||||
pkgMgr=""
|
||||
if command -v pacman &>/dev/null; then
|
||||
@@ -88,25 +132,50 @@ else
|
||||
fi
|
||||
|
||||
if [ -z "$noPkgMgr" ]; then
|
||||
info "Получение списка файлов с https://gitea.plemya-x.ru/Plemya-x/ALR/releases"
|
||||
info "Получение списка релизов через API Gitea"
|
||||
|
||||
# Изменено URL и регулярное выражение для списка файлов
|
||||
pageContent=$(curl -s https://gitea.plemya-x.ru/Plemya-x/ALR/releases)
|
||||
# Используем API для получения последнего релиза
|
||||
releases=$(curl -s "https://gitea.plemya-x.ru/api/v1/repos/Plemya-x/ALR/releases")
|
||||
|
||||
# Извлечение списка файлов из HTML
|
||||
fileList=$(echo "$pageContent" | grep -oP '(?<=href=").*?(?=")' | grep -E 'alr-bin.*\.(pkg.tar.zst|rpm|deb)')
|
||||
if [ -z "$releases" ] || [ "$releases" = "null" ]; then
|
||||
error "Не удалось получить список релизов. Проверьте соединение с интернетом."
|
||||
fi
|
||||
|
||||
echo "Полученный список файлов:"
|
||||
echo "$fileList"
|
||||
# Получаем URL последнего релиза
|
||||
latestReleaseUrl=$(echo "$releases" | grep -o '"browser_download_url":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
|
||||
if [ -z "$latestReleaseUrl" ]; then
|
||||
# Fallback на парсинг HTML если API не работает
|
||||
warn "API не доступен, пробуем получить список через HTML"
|
||||
pageContent=$(curl -s https://gitea.plemya-x.ru/Plemya-x/ALR/releases)
|
||||
fileList=$(echo "$pageContent" | grep -oP '(?<=href=")[^"]*alr-bin[^"]*\.(pkg\.tar\.zst|rpm|deb)' | sed 's|^|https://gitea.plemya-x.ru|')
|
||||
else
|
||||
# Получаем список файлов из API
|
||||
latestReleaseId=$(echo "$releases" | grep -o '"id":[0-9]*' | head -1 | cut -d':' -f2)
|
||||
assets=$(curl -s "https://gitea.plemya-x.ru/api/v1/repos/Plemya-x/ALR/releases/$latestReleaseId/assets")
|
||||
# Фильтруем только пакеты, исключая tar.gz архивы
|
||||
fileList=$(echo "$assets" | grep -o '"browser_download_url":"[^"]*"' | cut -d'"' -f4 | grep -v '\.tar\.gz$')
|
||||
fi
|
||||
|
||||
if [ -z "$fileList" ]; then
|
||||
warn "Не найдены готовые пакеты в последнем релизе"
|
||||
warn "Возможно, для вашего дистрибутива нужно собрать пакет из исходников"
|
||||
warn "Инструкции по сборке: https://gitea.plemya-x.ru/Plemya-x/ALR"
|
||||
error "Не удалось получить список пакетов для загрузки"
|
||||
fi
|
||||
|
||||
info "Получен список файлов релиза"
|
||||
|
||||
if [ "$pkgMgr" == "pacman" ]; then
|
||||
latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*\.pkg\.tar\.zst' | sort -V | tail -n 1)
|
||||
latestFile=$(echo "$fileList" | grep -E "alr-bin.*\.pkg\.tar\.zst" | sort -V | tail -n 1)
|
||||
elif [ "$pkgMgr" == "apt" ]; then
|
||||
latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*\.amd64\.deb' | sort -V | tail -n 1)
|
||||
latestFile=$(echo "$fileList" | grep -E "alr-bin.*\.${debArch}\.deb" | sort -V | tail -n 1)
|
||||
elif [[ "$pkgMgr" == "dnf" || "$pkgMgr" == "yum" || "$pkgMgr" == "zypper" ]]; then
|
||||
latestFile=$(printf "%s\n" "${fileList[@]}" | grep -E 'alr-bin-.*\.x86_64\.rpm' | grep -v 'alt[0-9]*' | sort -V | tail -n 1)
|
||||
latestFile=$(echo "$fileList" | grep -E "alr-bin.*\.${rpmArch}\.rpm" | grep -v 'alt[0-9]*' | sort -V | tail -n 1)
|
||||
elif [ "$pkgMgr" == "apt-get" ]; then
|
||||
latestFile=$(echo "$fileList" | grep -E 'alr-bin-.*-alt[0-9]+\.x86_64\.rpm' | sort -V | tail -n 1)
|
||||
latestFile=$(echo "$fileList" | grep -E "alr-bin.*-alt[0-9]+\.${rpmArch}\.rpm" | sort -V | tail -n 1)
|
||||
elif [ "$pkgMgr" == "apk" ]; then
|
||||
latestFile=$(echo "$fileList" | grep -E "alr-bin.*\.apk" | sort -V | tail -n 1)
|
||||
else
|
||||
error "Не поддерживаемый менеджер пакетов для автоматической установки"
|
||||
fi
|
||||
@@ -119,18 +188,35 @@ if [ -z "$noPkgMgr" ]; then
|
||||
|
||||
fname="$(mktemp -u -p /tmp "alr.XXXXXXXXXX").${pkgFormat}"
|
||||
|
||||
info "Загрузка пакета ALR"
|
||||
curl -o $fname -L "$latestFile"
|
||||
# Настраиваем trap для очистки временного файла
|
||||
trap "rm -f $fname" EXIT
|
||||
|
||||
if [ ! -f "$fname" ]; then
|
||||
error "Ошибка загрузки пакета ALR"
|
||||
info "Загрузка пакета ALR"
|
||||
info "URL: $latestFile"
|
||||
|
||||
# Загружаем с проверкой кода возврата
|
||||
if ! curl -f -L -o "$fname" "$latestFile"; then
|
||||
error "Ошибка загрузки пакета ALR. Проверьте подключение к интернету."
|
||||
fi
|
||||
|
||||
# Проверяем что файл не пустой
|
||||
if [ ! -s "$fname" ]; then
|
||||
error "Загруженный файл пустой или поврежден"
|
||||
fi
|
||||
|
||||
# Показываем размер загруженного файла
|
||||
fileSize=$(du -h "$fname" | cut -f1)
|
||||
info "Загружен пакет размером $fileSize"
|
||||
|
||||
info "Установка пакета ALR"
|
||||
installPkg "$pkgMgr" "$fname"
|
||||
|
||||
# Отправляем статистику установки
|
||||
trackInstallation
|
||||
|
||||
info "Очистка"
|
||||
rm "$fname"
|
||||
rm -f "$fname"
|
||||
trap - EXIT
|
||||
|
||||
info "Готово!"
|
||||
else
|
||||
|
38
scripts/test-coverage-precommit.sh
Executable file
38
scripts/test-coverage-precommit.sh
Executable file
@@ -0,0 +1,38 @@
|
||||
#!/bin/bash
|
||||
# ALR - Any Linux Repository
|
||||
# Copyright (C) 2025 The ALR Authors
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
set -e
|
||||
|
||||
# Запускаем тесты с покрытием
|
||||
make test-coverage
|
||||
|
||||
# coverage.out в .gitignore, не добавляем его
|
||||
# Но если скрипт coverage-badge.sh изменил какие-то файлы (например, README с бейджем),
|
||||
# они будут добавлены
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=M | grep -v '\.out$' | grep -v '^coverage' || true)
|
||||
|
||||
if [ ! -z "$CHANGED_FILES" ]; then
|
||||
echo "Test coverage updated the following files:"
|
||||
echo "$CHANGED_FILES"
|
||||
# Добавляем только измененные файлы, которые уже отслеживаются
|
||||
echo "$CHANGED_FILES" | xargs -r git add
|
||||
echo "Files were updated and staged"
|
||||
fi
|
||||
|
||||
echo "Tests completed successfully"
|
||||
# Всегда возвращаем успех если тесты прошли
|
||||
exit 0
|
@@ -29,7 +29,6 @@ import (
|
||||
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/search"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
|
||||
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
|
||||
)
|
||||
@@ -72,9 +71,6 @@ func SearchCmd() *cli.Command {
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx := c.Context
|
||||
|
||||
|
19
upgrade.go
19
upgrade.go
@@ -55,9 +55,6 @@ func UpgradeCmd() *cli.Command {
|
||||
},
|
||||
},
|
||||
Action: utils.RootNeededAction(func(c *cli.Context) error {
|
||||
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
installer, installerClose, err := build.GetSafeInstaller()
|
||||
if err != nil {
|
||||
@@ -65,9 +62,6 @@ func UpgradeCmd() *cli.Command {
|
||||
}
|
||||
defer installerClose()
|
||||
|
||||
if err := utils.ExitIfCantSetNoNewPrivs(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scripter, scripterClose, err := build.GetSafeScriptExecutor()
|
||||
if err != nil {
|
||||
@@ -90,6 +84,19 @@ func UpgradeCmd() *cli.Command {
|
||||
}
|
||||
defer deps.Defer()
|
||||
|
||||
// Обновляем систему, если это включено в конфигурации
|
||||
if deps.Cfg.UpdateSystemOnUpgrade() {
|
||||
slog.Info(gotext.Get("Updating system packages..."))
|
||||
err = deps.Manager.UpgradeAll(&manager.Opts{
|
||||
NoConfirm: !c.Bool("interactive"),
|
||||
Args: manager.Args,
|
||||
})
|
||||
if err != nil {
|
||||
return cliutils.FormatCliExit(gotext.Get("Error updating system packages"), err)
|
||||
}
|
||||
slog.Info(gotext.Get("System packages updated successfully"))
|
||||
}
|
||||
|
||||
builder, err := build.NewMainBuilder(
|
||||
deps.Cfg,
|
||||
deps.Manager,
|
||||
|
Reference in New Issue
Block a user