465 lines
17 KiB
Go
465 lines
17 KiB
Go
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
|
||
} |