Добавление первой итерации генерации плагина .star

This commit is contained in:
2025-09-11 17:43:21 +03:00
parent 990e091e8b
commit 5b2b370a39
8 changed files with 1017 additions and 16 deletions

83
Makefile Normal file
View File

@@ -0,0 +1,83 @@
.PHONY: build install clean test generate-plugins analyze-repo
# Версия и инфо
VERSION ?= $(shell git describe --tags --dirty --always 2>/dev/null || echo "dev")
BUILD_TIME ?= $(shell date -u '+%Y-%m-%d_%H:%M:%S')
GO_VERSION ?= $(shell go version | cut -d " " -f 3)
# Флаги линковки
LDFLAGS = -ldflags "\
-X main.Version=$(VERSION) \
-X main.BuildTime=$(BUILD_TIME) \
-X main.GoVersion=$(GO_VERSION) \
-s -w"
# Основные цели
build: alr-updater analyze-repo
alr-updater:
@echo "🔨 Building ALR-updater..."
CGO_ENABLED=1 go build $(LDFLAGS) -o alr-updater main.go
analyze-repo:
@echo "🔨 Building repository analyzer..."
cd cmd/analyze-repo && CGO_ENABLED=1 go build $(LDFLAGS) -o ../../analyze-repo .
install: build
@echo "📦 Installing ALR-updater..."
sudo install -m 755 alr-updater /usr/local/bin/
sudo install -m 755 analyze-repo /usr/local/bin/
@echo "✅ Installation complete!"
clean:
@echo "🧹 Cleaning..."
rm -f alr-updater analyze-repo
test:
@echo "🧪 Running tests..."
go test -v ./...
# Генерация плагинов
generate-plugins:
@echo "🤖 Generating missing plugins..."
./alr-updater --generate-plugins
# Анализ репозитория
analyze:
@echo "📊 Analyzing repository..."
./analyze-repo --format=table
analyze-json:
@echo "📊 Analyzing repository (JSON)..."
./analyze-repo --format=json
# Генерация через анализатор
generate-missing:
@echo "🤖 Generating missing plugins via analyzer..."
./analyze-repo --generate
# Помощь
help:
@echo "ALR-updater Build System"
@echo "========================"
@echo ""
@echo "Targets:"
@echo " build - Build all binaries"
@echo " alr-updater - Build main updater"
@echo " analyze-repo - Build repository analyzer"
@echo " install - Install binaries to /usr/local/bin"
@echo " clean - Remove built binaries"
@echo " test - Run tests"
@echo ""
@echo "Plugin Generation:"
@echo " generate-plugins - Generate missing plugins"
@echo " analyze - Analyze repository (table format)"
@echo " analyze-json - Analyze repository (JSON format)"
@echo " generate-missing - Analyze and generate missing plugins"
@echo ""
@echo "Variables:"
@echo " VERSION - Version string (default: git describe)"
@echo " BUILD_TIME - Build timestamp"
# Цель по умолчанию
all: build

BIN
analyze-repo Executable file

Binary file not shown.

BIN
cmd/analyze-repo/analyze-repo Executable file

Binary file not shown.

232
cmd/analyze-repo/main.go Normal file
View File

@@ -0,0 +1,232 @@
package main
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/pelletier/go-toml/v2"
"github.com/spf13/pflag"
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/generator"
)
type AnalysisResult struct {
PackageName string `json:"package_name"`
Version string `json:"version"`
GitHubRepo string `json:"github_repo,omitempty"`
Sources []string `json:"sources,omitempty"`
Description string `json:"description,omitempty"`
Homepage string `json:"homepage,omitempty"`
PackageType string `json:"package_type"`
HasPlugin bool `json:"has_plugin"`
PluginGenerated bool `json:"can_generate_plugin"`
}
func main() {
configPath := pflag.StringP("config", "c", "/etc/alr-updater/config.toml", "Path to config file")
pluginDir := pflag.StringP("plugin-dir", "p", "/etc/alr-updater/plugins", "Path to plugin directory")
repoName := pflag.StringP("repo", "r", "alr-repo", "Repository name to analyze")
outputFormat := pflag.StringP("format", "f", "table", "Output format: table, json")
generateMissing := pflag.BoolP("generate", "g", false, "Generate missing plugins")
pflag.Parse()
// Загружаем конфигурацию
cfg := &config.Config{}
fl, err := os.Open(*configPath)
if err != nil {
fmt.Printf("Error opening config file: %v\n", err)
os.Exit(1)
}
defer fl.Close()
err = toml.NewDecoder(fl).Decode(cfg)
if err != nil {
fmt.Printf("Error decoding config file: %v\n", err)
os.Exit(1)
}
// Создаем генератор плагинов
gen, err := generator.NewPluginGenerator(cfg, *pluginDir)
if err != nil {
fmt.Printf("Error creating plugin generator: %v\n", err)
os.Exit(1)
}
// Сканируем репозиторий
packages, err := gen.ScanRepository(*repoName)
if err != nil {
fmt.Printf("Error scanning repository: %v\n", err)
os.Exit(1)
}
// Получаем список существующих плагинов
existingPlugins := make(map[string]bool)
pluginFiles, err := filepath.Glob(filepath.Join(*pluginDir, "*.star"))
if err == nil {
for _, pluginFile := range pluginFiles {
pluginName := strings.TrimSuffix(filepath.Base(pluginFile), ".star")
existingPlugins[pluginName] = true
}
}
// Подготавливаем результаты анализа
var results []AnalysisResult
var canGenerate []generator.DetectedPackage
for _, pkg := range packages {
result := AnalysisResult{
PackageName: pkg.Name,
Version: pkg.Version,
GitHubRepo: pkg.GitHubRepo,
Sources: pkg.Sources,
Description: pkg.Description,
Homepage: pkg.Homepage,
PackageType: pkg.PackageType,
HasPlugin: existingPlugins[pkg.Name],
PluginGenerated: pkg.GitHubRepo != "" && pkg.PackageType == "github_release",
}
results = append(results, result)
if !result.HasPlugin && result.PluginGenerated {
canGenerate = append(canGenerate, pkg)
}
}
// Добавляем пакеты с существующими плагинами
repoPath := filepath.Join(cfg.ReposBaseDir, *repoName)
err = filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
if err != nil || info.Name() != "alr.sh" {
return err
}
packageName := filepath.Base(filepath.Dir(path))
if existingPlugins[packageName] {
// Проверяем, есть ли уже в результатах
found := false
for _, r := range results {
if r.PackageName == packageName {
found = true
break
}
}
if !found {
// Добавляем пакет с существующим плагином
pkgInfo := analyzePackageFile(path)
result := AnalysisResult{
PackageName: packageName,
Version: pkgInfo.Version,
Sources: pkgInfo.Sources,
Description: pkgInfo.Description,
Homepage: pkgInfo.Homepage,
PackageType: "has_plugin",
HasPlugin: true,
}
results = append(results, result)
}
}
return nil
})
// Выводим результаты
if *outputFormat == "json" {
output, _ := json.MarshalIndent(results, "", " ")
fmt.Println(string(output))
} else {
printTable(results, canGenerate)
}
// Генерируем недостающие плагины
if *generateMissing {
fmt.Printf("\n🚀 Generating %d missing plugins...\n", len(canGenerate))
for _, pkg := range canGenerate {
err := gen.GeneratePlugin(pkg, *repoName)
if err != nil {
fmt.Printf("❌ Error generating plugin for %s: %v\n", pkg.Name, err)
}
}
fmt.Printf("✅ Plugin generation completed!\n")
}
}
func analyzePackageFile(alrFile string) generator.PackageInfo {
content, err := os.ReadFile(alrFile)
if err != nil {
return generator.PackageInfo{Name: filepath.Base(filepath.Dir(alrFile))}
}
info := generator.PackageInfo{
Name: filepath.Base(filepath.Dir(alrFile)),
}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "version=") {
versionMatch := regexp.MustCompile(`version=['"]?([^'"]+)['"]?`).FindStringSubmatch(line)
if len(versionMatch) > 1 {
info.Version = versionMatch[1]
}
}
if strings.Contains(line, "github.com") || strings.Contains(line, "https://") {
urlMatch := regexp.MustCompile(`https://[^\s"']+`).FindAllString(line, -1)
info.Sources = append(info.Sources, urlMatch...)
}
if strings.HasPrefix(line, "desc=") {
descMatch := regexp.MustCompile(`desc=['"]?([^'"]+)['"]?`).FindStringSubmatch(line)
if len(descMatch) > 1 {
info.Description = descMatch[1]
}
}
}
return info
}
func printTable(results []AnalysisResult, canGenerate []generator.DetectedPackage) {
fmt.Printf("📊 Анализ репозитория\n")
fmt.Printf("═══════════════════════════════════════════════════════════════════════════════════\n")
withPlugins := 0
withoutPlugins := 0
canGenerateCount := len(canGenerate)
for _, r := range results {
if r.HasPlugin {
withPlugins++
} else {
withoutPlugins++
}
}
fmt.Printf("📦 Всего пакетов: %d\n", len(results))
fmt.Printf("✅ С плагинами: %d\n", withPlugins)
fmt.Printf("❌ Без плагинов: %d\n", withoutPlugins)
fmt.Printf("🤖 Можно сгенерировать: %d\n", canGenerateCount)
if canGenerateCount > 0 {
fmt.Printf("\n🎯 Пакеты для автогенерации:\n")
fmt.Printf("─────────────────────────────────────────────────────────────────────────────────\n")
for _, pkg := range canGenerate {
fmt.Printf("📦 %-25s │ %s\n", pkg.Name, pkg.GitHubRepo)
}
fmt.Printf("\n💡 Запустите с флагом --generate для создания плагинов\n")
}
if withoutPlugins > canGenerateCount {
fmt.Printf("\n⚠ Пакеты требующие ручной настройки:\n")
fmt.Printf("─────────────────────────────────────────────────────────────────────────────────\n")
for _, r := range results {
if !r.HasPlugin && !r.PluginGenerated {
fmt.Printf("📦 %-25s │ %s\n", r.PackageName, r.PackageType)
}
}
}
}

197
docs/plugin-generation.md Normal file
View File

@@ -0,0 +1,197 @@
# Автоматическая генерация плагинов
ALR-updater поддерживает автоматическую генерацию плагинов для пакетов, размещённых на GitHub. Система может автоматически обнаруживать пакеты без плагинов и создавать для них соответствующие .star файлы.
## Возможности
### Автоматическое обнаружение
- ✅ Сканирование репозиториев для поиска пакетов без плагинов
- ✅ Определение GitHub репозиториев из файлов alr.sh
- ✅ Классификация пакетов по типам (binary, source, git)
- ✅ Генерация подходящих шаблонов плагинов
### Поддерживаемые типы пакетов
- **Binary packages (-bin)**: AppImage, tar.gz, zip архивы
- **Source packages**: tar.gz архивы с исходным кодом
- **Library packages**: C++ библиотеки, фреймворки
- **Tool packages**: Утилиты командной строки
### Автоматическое определение
- 📦 **Версии**: Извлечение из тегов GitHub (v1.2.3, release-1.2.3)
- 🔗 **URL скачивания**: Поиск подходящих assets в релизах
- 📅 **Расписание**: Автоматический выбор частоты проверок
- 🔍 **Asset паттерны**: Распознавание типов файлов
## Использование
### 1. Встроенная генерация
```bash
# Генерация всех недостающих плагинов
./alr-updater --generate-plugins
# С пользовательскими путями
./alr-updater --generate-plugins \
--config=/path/to/config.toml \
--plugin-dir=/path/to/plugins/
```
### 2. Анализ репозитория
```bash
# Сборка анализатора
make analyze-repo
# Анализ репозитория (табличный формат)
./analyze-repo --repo=alr-repo
# JSON формат для программной обработки
./analyze-repo --repo=alr-repo --format=json
# Анализ + генерация недостающих плагинов
./analyze-repo --repo=alr-repo --generate
```
### 3. Через Makefile
```bash
# Анализ репозитория
make analyze
# Генерация недостающих плагинов
make generate-missing
# Анализ в JSON формате
make analyze-json
```
## Примеры вывода
### Табличный анализ
```
📊 Анализ репозитория
═══════════════════════════════════════════════════════════════════════════════════
📦 Всего пакетов: 67
С плагинами: 48
❌ Без плагинов: 19
🤖 Можно сгенерировать: 15
🎯 Пакеты для автогенерации:
─────────────────────────────────────────────────────────────────────────────────
📦 telegram-desktop-bin │ telegramdesktop/tdesktop
📦 obsidian-bin │ obsidianmd/obsidian-releases
📦 yarn │ yarnpkg/yarn
📦 electron-bin │ electron/electron
```
### JSON анализ
```json
[
{
"package_name": "telegram-desktop-bin",
"version": "5.0.1",
"github_repo": "telegramdesktop/tdesktop",
"sources": ["https://github.com/telegramdesktop/tdesktop/..."],
"description": "Official Telegram Desktop client",
"package_type": "github_release",
"has_plugin": false,
"can_generate_plugin": true
}
]
```
## Структура сгенерированных плагинов
Сгенерированные плагины включают:
```python
# Repository: alr-repo
# Auto-generated plugin for package-name
REPO = "alr-repo"
def check_package_name():
"""Проверка обновлений для package-name с автоматическим обновлением checksums"""
# Автоматическое определение версий
# GitHub API интеграция
# Поиск подходящих assets
# Обновление хеш-сумм
# Автоматические коммиты
# Умное расписание на основе типа пакета
run_every.day(check_package_name)
```
## Конфигурация
### Специальные случаи
Можно настроить специальные правила для пакетов:
```go
// internal/generator/plugin_generator.go
func (pg *PluginGenerator) detectBinaryPackage(packageName string, detected DetectedPackage) DetectedPackage {
switch {
case strings.Contains(packageName, "telegram"):
detected.AssetPattern = `tsetup\..*\.tar\.xz`
detected.URLTemplate = "https://github.com/telegramdesktop/tdesktop/releases/download/{tag}/tsetup.{version}.tar.xz"
case strings.Contains(packageName, "obsidian"):
detected.AssetPattern = `.*\.AppImage$`
// ... другие специальные случаи
}
}
```
### Расписание проверок
Автоматически определяется на основе типа пакета:
- **Binary packages**: Каждые 6 часов
- **Library packages**: Каждую неделю
- **Tool packages**: Каждый день
## Ограничения
### Текущие ограничения
- ❌ Только GitHub репозитории
- ❌ Требует стандартную структуру релизов
-Не поддерживает сложные схемы версионирования
### Планируемые улучшения
- 🔄 Поддержка GitLab, Gitea
- 🔄 Поддержка PyPI, npm, crates.io
- 🔄 Веб-интерфейс для настройки
- 🔄 Валидация сгенерированных плагинов
## Отладка
### Проверка сгенерированного плагина
```bash
# Проверка синтаксиса
./alr-updater --plugin-dir=./plugins --config=test-config.toml --debug
# Тестовый запуск
./alr-updater --now --debug
```
### Логирование
```bash
# Включить отладочные сообщения
./alr-updater --debug --generate-plugins
```
## Вклад в развитие
Для улучшения системы генерации:
1. **Добавление новых типов пакетов** в `detectPackageType()`
2. **Улучшение паттернов** в `detectBinaryPackage()`
3. **Новые источники** кроме GitHub
4. **Тестирование** на реальных пакетах
См. также:
- [Создание плагинов вручную](manual-plugins.md)
- [API документация](api.md)
- [Конфигурация](configuration.md)

View File

@@ -116,38 +116,46 @@ var runEveryModule = &starlarkstruct.Module{
func runEveryMinute(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var fn *starlark.Function
err := starlark.UnpackArgs("run_every.minute", args, kwargs, "function", &fn)
var count int64 = 1
err := starlark.UnpackArgs("run_every.minute", args, kwargs, "function", &fn, "count?", &count)
if err != nil {
return nil, err
}
return runScheduled(thread, fn, "1m")
duration := time.Duration(count) * time.Minute
return runScheduled(thread, fn, duration.String())
}
func runEveryHour(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var fn *starlark.Function
err := starlark.UnpackArgs("run_every.hour", args, kwargs, "function", &fn)
var count int64 = 1
err := starlark.UnpackArgs("run_every.hour", args, kwargs, "function", &fn, "count?", &count)
if err != nil {
return nil, err
}
return runScheduled(thread, fn, "1h")
duration := time.Duration(count) * time.Hour
return runScheduled(thread, fn, duration.String())
}
func runEveryDay(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var fn *starlark.Function
err := starlark.UnpackArgs("run_every.day", args, kwargs, "function", &fn)
var count int64 = 1
err := starlark.UnpackArgs("run_every.day", args, kwargs, "function", &fn, "count?", &count)
if err != nil {
return nil, err
}
return runScheduled(thread, fn, "24h")
duration := time.Duration(count) * 24 * time.Hour
return runScheduled(thread, fn, duration.String())
}
func runEveryWeek(thread *starlark.Thread, b *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) {
var fn *starlark.Function
err := starlark.UnpackArgs("run_every.week", args, kwargs, "function", &fn)
var count int64 = 1
err := starlark.UnpackArgs("run_every.week", args, kwargs, "function", &fn, "count?", &count)
if err != nil {
return nil, err
}
return runScheduled(thread, fn, "168h")
duration := time.Duration(count) * 7 * 24 * time.Hour
return runScheduled(thread, fn, duration.String())
}
func runScheduled(thread *starlark.Thread, fn *starlark.Function, duration string) (starlark.Value, error) {

View File

@@ -0,0 +1,465 @@
package generator
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"text/template"
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/config"
)
// PluginTemplate представляет шаблон плагина
type PluginTemplate struct {
PackageName string
FunctionName string
Repository string
GitHubRepo string
AssetPattern string
URLTemplate string
URLPattern string
VersionPrefix string
Schedule string
ScheduleCalls string
}
// PackageInfo содержит информацию о пакете из alr.sh
type PackageInfo struct {
Name string
Version string
Sources []string
Description string
Homepage string
}
// DetectedPackage содержит автоматически определенную информацию о пакете
type DetectedPackage struct {
PackageInfo
GitHubRepo string
PackageType string
AssetPattern string
URLTemplate string
VersionPrefix string
}
const pluginTemplateStr = `# Repository: {{.Repository}}
# Auto-generated plugin for {{.PackageName}}
REPO = "{{.Repository}}"
def check_{{.FunctionName}}():
"""Проверка обновлений для {{.PackageName}} с автоматическим обновлением checksums"""
package_name = "{{.PackageName}}"
# Обновляем репозиторий перед проверкой
updater.pull(REPO)
# Получение текущей версии из пакета
current_content = updater.get_package_file(REPO, package_name, "alr.sh")
# Универсальное распознавание: без кавычек, одинарные кавычки, двойные кавычки
current_version_matches = regex.find(r"version=['\"]?([0-9.]+)['\"]?", current_content)
if not current_version_matches or len(current_version_matches) == 0:
log.error("Не удалось найти текущую версию для " + package_name)
return
current_version = current_version_matches[0]
# Проверка новой версии через GitHub API
api_url = "https://api.github.com/repos/{{.GitHubRepo}}/releases/latest"
response = http.get(api_url)
if not response:
log.error("Не удалось получить ответ от GitHub API для " + package_name)
return
release_data = response.json()
if not release_data or "tag_name" not in release_data:
log.error("Нет данных о релизе для " + package_name)
return
# Извлекаем версию из тега{{if .VersionPrefix}} (убираем '{{.VersionPrefix}}' если есть){{end}}
tag_name = release_data["tag_name"]
{{if .VersionPrefix}}new_version = tag_name[{{len .VersionPrefix}}:] if tag_name.startswith("{{.VersionPrefix}}") else tag_name{{else}}new_version = tag_name[1:] if tag_name.startswith("v") else tag_name{{end}}
# Сравнение версий
if new_version != current_version:
log.info("Обнаружено обновление " + package_name + ": " + current_version + " -> " + new_version)
{{if .URLTemplate}}# URL для скачивания по шаблону
download_url = "{{.URLTemplate}}".replace("{tag}", tag_name).replace("{version}", new_version){{else if .AssetPattern}}# Находим URL для файла по паттерну
download_url = ""
if "assets" in release_data:
for asset in release_data["assets"]:
asset_matches = regex.find(r"{{.AssetPattern}}", asset["name"])
if asset_matches and len(asset_matches) > 0:
download_url = asset["browser_download_url"]
break
if not download_url:
log.error("Не найден подходящий asset для " + package_name)
return{{else}}# Ищем первый подходящий asset
download_url = ""
if "assets" in release_data and len(release_data["assets"]) > 0:
download_url = release_data["assets"][0]["browser_download_url"]{{end}}
# Обновление версии в файле (сохраняем формат кавычек)
# Определяем какой формат используется
if regex.find(r"version='[0-9.]+'", current_content):
# Одинарные кавычки
new_content = regex.replace(
current_content,
r"version='[0-9.]+'",
"version='" + new_version + "'"
)
elif regex.find(r'version="[0-9.]+"', current_content):
# Двойные кавычки
new_content = regex.replace(
current_content,
r'version="[0-9.]+"',
'version="' + new_version + '"'
)
else:
# Без кавычек
new_content = regex.replace(
current_content,
r"version=[0-9.]+",
"version=" + new_version
)
{{if .URLPattern}}# Обновление URL загрузки в файле
new_content = regex.replace(
new_content,
r"{{.URLPattern}}",
download_url
){{end}}
# Вычисление новой хеш-суммы
log.info("Вычисление SHA256 для " + download_url)
new_checksum = checksum.calculate_sha256(download_url)
log.info("Новая хеш-сумма: " + new_checksum)
# Обновление файла с новыми checksums
updater.update_checksums(
REPO,
package_name,
"alr.sh",
new_content,
[new_checksum]
)
# Коммит и push изменений
updater.push_changes(REPO, package_name + ": обновление до версии " + new_version)
log.info("Пакет " + package_name + " успешно обновлён до версии " + new_version + " с новой хеш-суммой")
else:
log.info("Пакет " + package_name + " уже имеет последнюю версию: " + current_version)
# {{.Schedule}}
{{.ScheduleCalls}}`
// PluginGenerator представляет генератор плагинов
type PluginGenerator struct {
config *config.Config
template *template.Template
reposDir string
pluginsDir string
}
// NewPluginGenerator создает новый генератор плагинов
func NewPluginGenerator(cfg *config.Config, pluginsDir string) (*PluginGenerator, error) {
tmpl, err := template.New("plugin").Parse(pluginTemplateStr)
if err != nil {
return nil, fmt.Errorf("failed to parse template: %w", err)
}
return &PluginGenerator{
config: cfg,
template: tmpl,
reposDir: cfg.ReposBaseDir,
pluginsDir: pluginsDir,
}, nil
}
// ScanRepository сканирует репозиторий и находит пакеты без плагинов
func (pg *PluginGenerator) ScanRepository(repoName string) ([]DetectedPackage, error) {
repoPath := filepath.Join(pg.reposDir, repoName)
if _, err := os.Stat(repoPath); os.IsNotExist(err) {
return nil, fmt.Errorf("repository directory not found: %s", repoPath)
}
var packages []DetectedPackage
err := filepath.Walk(repoPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Ищем файлы alr.sh
if info.Name() == "alr.sh" {
packageDir := filepath.Dir(path)
packageName := filepath.Base(packageDir)
// Пропускаем если плагин уже существует
pluginFile := filepath.Join(pg.pluginsDir, packageName+".star")
if _, err := os.Stat(pluginFile); err == nil {
return nil
}
// Анализируем пакет
pkgInfo, err := pg.analyzePackage(path)
if err != nil {
fmt.Printf("Warning: failed to analyze package %s: %v\n", packageName, err)
return nil
}
detected := pg.detectPackageType(packageName, *pkgInfo)
if detected.GitHubRepo != "" {
packages = append(packages, detected)
}
}
return nil
})
return packages, err
}
// analyzePackage анализирует файл alr.sh пакета
func (pg *PluginGenerator) analyzePackage(alrFile string) (*PackageInfo, error) {
content, err := os.ReadFile(alrFile)
if err != nil {
return nil, err
}
info := &PackageInfo{
Name: filepath.Base(filepath.Dir(alrFile)),
}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
line = strings.TrimSpace(line)
// Извлекаем версию
if strings.HasPrefix(line, "version=") {
versionMatch := regexp.MustCompile(`version=['"]?([^'"]+)['"]?`).FindStringSubmatch(line)
if len(versionMatch) > 1 {
info.Version = versionMatch[1]
}
}
// Извлекаем источники
if strings.Contains(line, "github.com") || strings.Contains(line, "https://") {
urlMatch := regexp.MustCompile(`https://[^\s"']+`).FindAllString(line, -1)
info.Sources = append(info.Sources, urlMatch...)
}
// Извлекаем описание
if strings.HasPrefix(line, "desc=") {
descMatch := regexp.MustCompile(`desc=['"]?([^'"]+)['"]?`).FindStringSubmatch(line)
if len(descMatch) > 1 {
info.Description = descMatch[1]
}
}
// Извлекаем homepage
if strings.HasPrefix(line, "homepage=") {
homepageMatch := regexp.MustCompile(`homepage=['"]?([^'"]+)['"]?`).FindStringSubmatch(line)
if len(homepageMatch) > 1 {
info.Homepage = homepageMatch[1]
}
}
}
return info, nil
}
// detectPackageType определяет тип пакета и GitHub репозиторий
func (pg *PluginGenerator) detectPackageType(packageName string, info PackageInfo) DetectedPackage {
detected := DetectedPackage{
PackageInfo: info,
PackageType: "unknown",
}
// Анализируем источники для GitHub репозиториев
githubRegex := regexp.MustCompile(`github\.com/([^/]+/[^/]+)`)
for _, source := range info.Sources {
if match := githubRegex.FindStringSubmatch(source); len(match) > 1 {
detected.GitHubRepo = match[1]
detected.PackageType = "github_release"
break
}
}
// Если не найдено в источниках, проверяем homepage
if detected.GitHubRepo == "" && info.Homepage != "" {
if match := githubRegex.FindStringSubmatch(info.Homepage); len(match) > 1 {
detected.GitHubRepo = match[1]
detected.PackageType = "github_release"
}
}
// Определяем паттерны для разных типов пакетов
if strings.HasSuffix(packageName, "-bin") {
detected = pg.detectBinaryPackage(packageName, detected)
} else if strings.HasSuffix(packageName, "-git") {
detected.PackageType = "git_commits"
} else {
detected = pg.detectSourcePackage(packageName, detected)
}
return detected
}
// detectBinaryPackage определяет паттерны для бинарных пакетов
func (pg *PluginGenerator) detectBinaryPackage(packageName string, detected DetectedPackage) DetectedPackage {
switch {
case strings.Contains(packageName, "telegram"):
detected.AssetPattern = `tsetup\..*\.tar\.xz`
detected.URLTemplate = "https://github.com/telegramdesktop/tdesktop/releases/download/{tag}/tsetup.{version}.tar.xz"
case strings.Contains(packageName, "discord"):
detected.AssetPattern = `.*\.tar\.gz$`
case strings.Contains(packageName, "obsidian"):
detected.AssetPattern = `.*\.AppImage$`
case strings.Contains(packageName, "electron"):
detected.AssetPattern = `.*-linux-x64\.zip$`
case strings.Contains(packageName, "vscode") || strings.Contains(packageName, "code"):
detected.AssetPattern = `.*\.tar\.gz$`
default:
// Общие паттерны для бинарных пакетов
detected.AssetPattern = `.*linux.*\.(tar\.gz|tar\.xz|zip|AppImage)$`
}
return detected
}
// detectSourcePackage определяет паттерны для исходных пакетов
func (pg *PluginGenerator) detectSourcePackage(packageName string, detected DetectedPackage) DetectedPackage {
// Для большинства исходных пакетов используем архив тега
detected.URLTemplate = "https://github.com/{repo}/archive/{tag}.tar.gz"
// Специальные случаи
switch packageName {
case "md4c":
detected.VersionPrefix = "release-"
case "steamcmd":
detected.PackageType = "http_lastmodified"
}
return detected
}
// GeneratePlugin генерирует плагин для пакета
func (pg *PluginGenerator) GeneratePlugin(detected DetectedPackage, repoName string) error {
functionName := strings.ReplaceAll(strings.ReplaceAll(detected.Name, "-", "_"), ".", "_")
schedule := pg.determineSchedule(detected)
scheduleCalls := pg.generateScheduleCalls(functionName, schedule)
data := PluginTemplate{
PackageName: detected.Name,
FunctionName: functionName,
Repository: repoName,
GitHubRepo: detected.GitHubRepo,
AssetPattern: detected.AssetPattern,
URLTemplate: detected.URLTemplate,
URLPattern: pg.generateURLPattern(detected),
VersionPrefix: detected.VersionPrefix,
Schedule: schedule,
ScheduleCalls: scheduleCalls,
}
pluginFile := filepath.Join(pg.pluginsDir, detected.Name+".star")
file, err := os.Create(pluginFile)
if err != nil {
return fmt.Errorf("failed to create plugin file: %w", err)
}
defer file.Close()
err = pg.template.Execute(file, data)
if err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
fmt.Printf("✅ Generated plugin: %s\n", pluginFile)
return nil
}
// determineSchedule определяет расписание проверки обновлений
func (pg *PluginGenerator) determineSchedule(detected DetectedPackage) string {
switch {
case strings.HasSuffix(detected.Name, "-bin"):
return "Запуск проверки каждые 6 часов"
case strings.Contains(detected.Name, "lib") || strings.Contains(detected.Name, "cmake"):
return "Запуск проверки каждую неделю"
default:
return "Запуск проверки каждый день"
}
}
// generateScheduleCalls генерирует вызовы функций расписания
func (pg *PluginGenerator) generateScheduleCalls(functionName, schedule string) string {
switch {
case strings.Contains(schedule, "6 часов"):
return fmt.Sprintf("run_every.hour(check_%s, 6)", functionName)
case strings.Contains(schedule, "день"):
return fmt.Sprintf("run_every.day(check_%s)", functionName)
case strings.Contains(schedule, "неделю"):
return fmt.Sprintf("run_every.week(check_%s)", functionName)
default:
return fmt.Sprintf("run_every.day(check_%s)", functionName)
}
}
// generateURLPattern генерирует regex паттерн для обновления URL в файле
func (pg *PluginGenerator) generateURLPattern(detected DetectedPackage) string {
if detected.URLTemplate != "" {
return "" // URL pattern будет сгенерирован автоматически
}
// Базовые паттерны для разных типов
switch detected.GitHubRepo {
case "telegramdesktop/tdesktop":
return `https://github\.com/telegramdesktop/tdesktop/releases/download/[^/]+/tsetup\.[0-9.]+\.tar\.xz`
case "obsidianmd/obsidian-releases":
return `https://github\.com/obsidianmd/obsidian-releases/releases/download/[^/]+/Obsidian-[0-9.]+\.AppImage`
default:
return `https://github\.com/` + strings.ReplaceAll(detected.GitHubRepo, "/", `\/`) + `/releases/download/[^/]+/.*`
}
}
// GenerateAllPlugins сканирует все репозитории и генерирует недостающие плагины
func (pg *PluginGenerator) GenerateAllPlugins() error {
var totalGenerated int
for repoName := range pg.config.Repositories {
fmt.Printf("🔍 Сканирование репозитория %s...\n", repoName)
packages, err := pg.ScanRepository(repoName)
if err != nil {
fmt.Printf("Warning: failed to scan repository %s: %v\n", repoName, err)
continue
}
fmt.Printf("Найдено %d пакетов без плагинов\n", len(packages))
for _, pkg := range packages {
if pkg.GitHubRepo == "" {
fmt.Printf("⚠️ Пропущен %s: GitHub репозиторий не определен\n", pkg.Name)
continue
}
err := pg.GeneratePlugin(pkg, repoName)
if err != nil {
fmt.Printf("❌ Ошибка генерации плагина для %s: %v\n", pkg.Name, err)
continue
}
totalGenerated++
}
}
fmt.Printf("\n🎉 Всего сгенерировано %d плагинов\n", totalGenerated)
return nil
}

34
main.go
View File

@@ -36,6 +36,7 @@ import (
"go.elara.ws/logger/log"
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/builtins"
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR-updater/internal/generator"
"go.etcd.io/bbolt"
"go.starlark.net/starlark"
"golang.org/x/crypto/bcrypt"
@@ -85,6 +86,7 @@ func main() {
useEnv := pflag.BoolP("use-env", "E", false, "Use environment variables for configuration")
debug := pflag.BoolP("debug", "D", false, "Enable debug logging")
runNow := pflag.BoolP("now", "n", false, "Run all plugin checks immediately on startup")
generatePlugins := pflag.Bool("generate-plugins", false, "Generate missing plugins automatically")
pflag.Parse()
if *debug {
@@ -105,14 +107,10 @@ func main() {
return
}
db, err := bbolt.Open(*dbPath, 0o644, nil)
if err != nil {
log.Fatal("Error opening database").Err(err).Send()
}
// Загружаем конфигурацию сначала для генератора плагинов
cfg := &config.Config{}
if *useEnv {
err = env.Parse(cfg)
err := env.Parse(cfg)
if err != nil {
log.Fatal("Error parsing environment variables").Err(err).Send()
}
@@ -125,10 +123,28 @@ func main() {
if err != nil {
log.Fatal("Error decoding config file").Err(err).Send()
}
err = fl.Close()
if err != nil {
log.Fatal("Error closing config file").Err(err).Send()
fl.Close()
}
// Обработка генерации плагинов
if *generatePlugins {
log.Info("Starting automatic plugin generation...")
gen, err := generator.NewPluginGenerator(cfg, *pluginDir)
if err != nil {
log.Fatal("Error creating plugin generator").Err(err).Send()
}
err = gen.GenerateAllPlugins()
if err != nil {
log.Fatal("Error generating plugins").Err(err).Send()
}
log.Info("Plugin generation completed")
return
}
db, err := bbolt.Open(*dbPath, 0o644, nil)
if err != nil {
log.Fatal("Error opening database").Err(err).Send()
}
// Создаем базовый каталог для репозиториев