3 Commits

Author SHA1 Message Date
8bc82cb95c Добавление статистики
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m56s
Create Release / changelog (push) Successful in 3m27s
Исправление работы с мультипакетами
2025-09-01 01:32:43 +03:00
9783ce37de Добавление возможности обновления системным пакетным менеджером при alr up
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m39s
2025-08-28 12:03:14 +03:00
b852688ab0 Исправление фильтрации имён пакетов в скрипте установки
All checks were successful
Pre-commit / pre-commit (push) Successful in 5m27s
2025-08-27 12:49:03 +03:00
10 changed files with 312 additions and 22 deletions

View File

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

View File

@@ -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"
@@ -401,9 +402,19 @@ func (b *Builder) BuildPackage(
// We filter so as not to re-build what has already been built at the `installBuildDeps` stage.
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 +539,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 +570,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 +711,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 +727,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

View File

@@ -103,22 +103,62 @@ func ShowScript(path, name, style string) error {
// FlattenPkgs attempts to flatten the a map of slices of packages into a single slice
// 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 {

View File

@@ -56,6 +56,7 @@ func defaultConfigKoanf() *koanf.Koanf {
"ignorePkgUpdates": []string{},
"logLevel": "info",
"autoPull": true,
"updateSystemOnUpgrade": false,
"repos": []types.Repo{
{
Name: "alr-default",
@@ -114,6 +115,11 @@ func (c *ALRConfig) Load() error {
}
}
// Выполняем миграцию конфигурации при необходимости
if err := c.migrateConfig(); err != nil {
return fmt.Errorf("failed to migrate config: %w", err)
}
return nil
}
@@ -125,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 }
@@ -133,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 }

View File

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

View File

@@ -60,6 +60,13 @@ func (rs *Repos) FindPkgs(ctx context.Context, pkgs []string) (map[string][]alrs
return nil, nil, fmt.Errorf("FindPkgs: get by provides: %w", err)
}
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
View File

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

View File

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

View File

@@ -56,6 +56,31 @@ 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
@@ -142,16 +167,15 @@ if [ -z "$noPkgMgr" ]; then
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-.*\.${debArch}\.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=$(echo "$fileList" | grep -E "alr-bin-.*\.${rpmArch}\.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
# ALT Linux использует RPM с особой маркировкой
latestFile=$(echo "$fileList" | grep -E "alr-bin-.*-alt[0-9]+\.${rpmArch}\.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)
latestFile=$(echo "$fileList" | grep -E "alr-bin.*\.apk" | sort -V | tail -n 1)
else
error "Не поддерживаемый менеджер пакетов для автоматической установки"
fi
@@ -187,6 +211,9 @@ if [ -z "$noPkgMgr" ]; then
info "Установка пакета ALR"
installPkg "$pkgMgr" "$fname"
# Отправляем статистику установки
trackInstallation
info "Очистка"
rm -f "$fname"
trap - EXIT

View File

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