diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..81fac34 --- /dev/null +++ b/Makefile @@ -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 \ No newline at end of file diff --git a/analyze-repo b/analyze-repo new file mode 100755 index 0000000..f531b35 Binary files /dev/null and b/analyze-repo differ diff --git a/cmd/analyze-repo/analyze-repo b/cmd/analyze-repo/analyze-repo new file mode 100755 index 0000000..3b4c7a9 Binary files /dev/null and b/cmd/analyze-repo/analyze-repo differ diff --git a/cmd/analyze-repo/main.go b/cmd/analyze-repo/main.go new file mode 100644 index 0000000..ff69f1b --- /dev/null +++ b/cmd/analyze-repo/main.go @@ -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) + } + } + } +} \ No newline at end of file diff --git a/docs/plugin-generation.md b/docs/plugin-generation.md new file mode 100644 index 0000000..575be2a --- /dev/null +++ b/docs/plugin-generation.md @@ -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) \ No newline at end of file diff --git a/internal/builtins/run_every.go b/internal/builtins/run_every.go index 7fc237a..d6eb168 100644 --- a/internal/builtins/run_every.go +++ b/internal/builtins/run_every.go @@ -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) { diff --git a/internal/generator/plugin_generator.go b/internal/generator/plugin_generator.go new file mode 100644 index 0000000..94ad290 --- /dev/null +++ b/internal/generator/plugin_generator.go @@ -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 +} \ No newline at end of file diff --git a/main.go b/main.go index 30e81d7..4240bf7 100644 --- a/main.go +++ b/main.go @@ -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() + fl.Close() + } + + // Обработка генерации плагинов + if *generatePlugins { + log.Info("Starting automatic plugin generation...") + gen, err := generator.NewPluginGenerator(cfg, *pluginDir) if err != nil { - log.Fatal("Error closing config file").Err(err).Send() + 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() } // Создаем базовый каталог для репозиториев