Some checks failed
		
		
	
	Pre-commit / pre-commit (push) Failing after 5m42s
				
			
		
			
				
	
	
		
			663 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			663 lines
		
	
	
		
			24 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
 | ||
| // It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
 | ||
| //
 | ||
| // ALR - Any Linux Repository
 | ||
| // Copyright (C) 2025 The ALR Authors
 | ||
| //
 | ||
| // This program is free software: you can redistribute it and/or modify
 | ||
| // it under the terms of the GNU General Public License as published by
 | ||
| // the Free Software Foundation, either version 3 of the License, or
 | ||
| // (at your option) any later version.
 | ||
| //
 | ||
| // This program is distributed in the hope that it will be useful,
 | ||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of
 | ||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | ||
| // GNU General Public License for more details.
 | ||
| //
 | ||
| // You should have received a copy of the GNU General Public License
 | ||
| // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | ||
| 
 | ||
| package gen
 | ||
| 
 | ||
| import (
 | ||
| 	_ "embed"
 | ||
| 	"encoding/json"
 | ||
| 	"fmt"
 | ||
| 	"io"
 | ||
| 	"net/http"
 | ||
| 	"net/url"
 | ||
| 	"regexp"
 | ||
| 	"strings"
 | ||
| 	"text/template"
 | ||
| )
 | ||
| 
 | ||
| // Встраиваем шаблон для AUR пакетов
 | ||
| //
 | ||
| //go:embed tmpls/aur.tmpl.sh
 | ||
| var aurTmpl string
 | ||
| 
 | ||
| // AUROptions содержит параметры для генерации шаблона AUR
 | ||
| type AUROptions struct {
 | ||
| 	Name    string // Имя пакета в AUR
 | ||
| 	Version string // Версия пакета (опционально, если не указана - берется последняя)
 | ||
| 	CreateDir bool  // Создавать ли директорию для пакета и дополнительные файлы
 | ||
| }
 | ||
| 
 | ||
| // aurAPIResponse представляет структуру ответа от API AUR
 | ||
| type aurAPIResponse struct {
 | ||
| 	Version      int         `json:"version"`      // Версия API
 | ||
| 	Type         string      `json:"type"`         // Тип ответа
 | ||
| 	ResultCount  int         `json:"resultcount"`  // Количество результатов
 | ||
| 	Results      []aurResult `json:"results"`      // Массив результатов
 | ||
| 	Error        string      `json:"error"`        // Сообщение об ошибке (если есть)
 | ||
| }
 | ||
| 
 | ||
| // aurResult содержит информацию о пакете из AUR
 | ||
| type aurResult struct {
 | ||
| 	ID             int      `json:"ID"`
 | ||
| 	Name           string   `json:"Name"`
 | ||
| 	PackageBaseID  int      `json:"PackageBaseID"`
 | ||
| 	PackageBase    string   `json:"PackageBase"`
 | ||
| 	Version        string   `json:"Version"`
 | ||
| 	Description    string   `json:"Description"`
 | ||
| 	URL            string   `json:"URL"`
 | ||
| 	NumVotes       int      `json:"NumVotes"`
 | ||
| 	Popularity     float64  `json:"Popularity"`
 | ||
| 	OutOfDate      *int     `json:"OutOfDate"`
 | ||
| 	Maintainer     string   `json:"Maintainer"`
 | ||
| 	FirstSubmitted int      `json:"FirstSubmitted"`
 | ||
| 	LastModified   int      `json:"LastModified"`
 | ||
| 	URLPath        string   `json:"URLPath"`
 | ||
| 	License        []string `json:"License"`
 | ||
| 	Keywords       []string `json:"Keywords"`
 | ||
| 	Depends        []string `json:"Depends"`
 | ||
| 	MakeDepends    []string `json:"MakeDepends"`
 | ||
| 	OptDepends     []string `json:"OptDepends"`
 | ||
| 	CheckDepends   []string `json:"CheckDepends"`
 | ||
| 	Conflicts      []string `json:"Conflicts"`
 | ||
| 	Provides       []string `json:"Provides"`
 | ||
| 	Replaces       []string `json:"Replaces"`
 | ||
| 	// Дополнительные поля для данных из PKGBUILD
 | ||
| 	Sources      []string `json:"-"`
 | ||
| 	Checksums    []string `json:"-"`
 | ||
| 	BuildFunc    string   `json:"-"`
 | ||
| 	PackageFunc  string   `json:"-"`
 | ||
| 	PrepareFunc  string   `json:"-"`
 | ||
| 	PackageType  string   `json:"-"`  // python, go, rust, cpp, nodejs, bin, git
 | ||
| 	HasDesktop   bool     `json:"-"`  // Есть ли desktop файлы
 | ||
| 	HasSystemd   bool     `json:"-"`  // Есть ли systemd сервисы
 | ||
| 	HasVersion   bool     `json:"-"`  // Есть ли функция version()
 | ||
| 	HasScripts   []string `json:"-"`  // Дополнительные скрипты (postinstall, postremove, etc)
 | ||
| 	HasPatches   bool     `json:"-"`  // Есть ли патчи
 | ||
| 	Architectures []string `json:"-"` // Поддерживаемые архитектуры
 | ||
| 	
 | ||
| 	// Автоматически определяемые файлы для install-* команд
 | ||
| 	BinaryFiles  []string `json:"-"`  // Исполняемые файлы для install-binary
 | ||
| 	LicenseFiles []string `json:"-"`  // Лицензионные файлы для install-license
 | ||
| 	ManualFiles  []string `json:"-"`  // Man страницы для install-manual
 | ||
| 	DesktopFiles []string `json:"-"`  // Desktop файлы для install-desktop
 | ||
| 	ServiceFiles []string `json:"-"`  // Systemd сервисы для install-systemd
 | ||
| 	CompletionFiles map[string]string `json:"-"` // Файлы автодополнения по типу (bash, zsh, fish)
 | ||
| }
 | ||
| 
 | ||
| // Вспомогательные методы для шаблона
 | ||
| func (r aurResult) LicenseString() string {
 | ||
| 	if len(r.License) == 0 {
 | ||
| 		return "custom:Unknown"
 | ||
| 	}
 | ||
| 	// Форматируем лицензии для alr.sh
 | ||
| 	licenses := make([]string, len(r.License))
 | ||
| 	for i, l := range r.License {
 | ||
| 		licenses[i] = fmt.Sprintf("'%s'", l)
 | ||
| 	}
 | ||
| 	return strings.Join(licenses, " ")
 | ||
| }
 | ||
| 
 | ||
| func (r aurResult) DependsString() string {
 | ||
| 	if len(r.Depends) == 0 {
 | ||
| 		return ""
 | ||
| 	}
 | ||
| 	deps := make([]string, len(r.Depends))
 | ||
| 	for i, d := range r.Depends {
 | ||
| 		// Убираем версионные ограничения для простоты
 | ||
| 		dep := strings.Split(d, ">=")[0]
 | ||
| 		dep = strings.Split(dep, "<=")[0]
 | ||
| 		dep = strings.Split(dep, "=")[0]
 | ||
| 		dep = strings.Split(dep, ">")[0]
 | ||
| 		dep = strings.Split(dep, "<")[0]
 | ||
| 		deps[i] = fmt.Sprintf("'%s'", dep)
 | ||
| 	}
 | ||
| 	return strings.Join(deps, " ")
 | ||
| }
 | ||
| 
 | ||
| func (r aurResult) MakeDependsString() string {
 | ||
| 	if len(r.MakeDepends) == 0 {
 | ||
| 		return ""
 | ||
| 	}
 | ||
| 	deps := make([]string, len(r.MakeDepends))
 | ||
| 	for i, d := range r.MakeDepends {
 | ||
| 		// Убираем версионные ограничения для простоты
 | ||
| 		dep := strings.Split(d, ">=")[0]
 | ||
| 		dep = strings.Split(dep, "<=")[0]
 | ||
| 		dep = strings.Split(dep, "=")[0]
 | ||
| 		dep = strings.Split(dep, ">")[0]
 | ||
| 		dep = strings.Split(dep, "<")[0]
 | ||
| 		deps[i] = fmt.Sprintf("'%s'", dep)
 | ||
| 	}
 | ||
| 	return strings.Join(deps, " ")
 | ||
| }
 | ||
| 
 | ||
| func (r aurResult) GitURL() string {
 | ||
| 	// Формируем URL для клонирования из AUR
 | ||
| 	return fmt.Sprintf("https://aur.archlinux.org/%s.git", r.PackageBase)
 | ||
| }
 | ||
| 
 | ||
| func (r aurResult) ArchitecturesString() string {
 | ||
| 	if len(r.Architectures) == 0 {
 | ||
| 		return "'all'"
 | ||
| 	}
 | ||
| 	archs := make([]string, len(r.Architectures))
 | ||
| 	for i, arch := range r.Architectures {
 | ||
| 		archs[i] = fmt.Sprintf("'%s'", arch)
 | ||
| 	}
 | ||
| 	return strings.Join(archs, " ")
 | ||
| }
 | ||
| 
 | ||
| func (r aurResult) OptDependsString() string {
 | ||
| 	if len(r.OptDepends) == 0 {
 | ||
| 		return ""
 | ||
| 	}
 | ||
| 	optDeps := make([]string, 0, len(r.OptDepends))
 | ||
| 	for _, dep := range r.OptDepends {
 | ||
| 		// Форматируем опциональные зависимости для alr.sh
 | ||
| 		parts := strings.SplitN(dep, ": ", 2)
 | ||
| 		if len(parts) == 2 {
 | ||
| 			optDeps = append(optDeps, fmt.Sprintf("'%s: %s'", parts[0], parts[1]))
 | ||
| 		} else {
 | ||
| 			optDeps = append(optDeps, fmt.Sprintf("'%s'", dep))
 | ||
| 		}
 | ||
| 	}
 | ||
| 	return strings.Join(optDeps, "\n\t")
 | ||
| }
 | ||
| 
 | ||
| func (r aurResult) ScriptsString() string {
 | ||
| 	if len(r.HasScripts) == 0 {
 | ||
| 		return ""
 | ||
| 	}
 | ||
| 	scripts := make([]string, len(r.HasScripts))
 | ||
| 	for i, script := range r.HasScripts {
 | ||
| 		scripts[i] = fmt.Sprintf("['%s']='%s.sh'", script, script)
 | ||
| 	}
 | ||
| 	return strings.Join(scripts, "\n\t")
 | ||
| }
 | ||
| 
 | ||
| // GenerateInstallCommands генерирует команды install-* для шаблона
 | ||
| func (r aurResult) GenerateInstallCommands() string {
 | ||
| 	var commands []string
 | ||
| 	
 | ||
| 	// install-binary команды
 | ||
| 	for _, binary := range r.BinaryFiles {
 | ||
| 		if binary == "./"+r.Name {
 | ||
| 			commands = append(commands, fmt.Sprintf("\tinstall-binary %s", binary))
 | ||
| 		} else {
 | ||
| 			commands = append(commands, fmt.Sprintf("\tinstall-binary %s %s", binary, r.Name))
 | ||
| 		}
 | ||
| 	}
 | ||
| 	
 | ||
| 	// install-license команды
 | ||
| 	for _, license := range r.LicenseFiles {
 | ||
| 		commands = append(commands, fmt.Sprintf("\tinstall-license %s %s/LICENSE", license, r.Name))
 | ||
| 	}
 | ||
| 	
 | ||
| 	// install-manual команды
 | ||
| 	for _, manual := range r.ManualFiles {
 | ||
| 		commands = append(commands, fmt.Sprintf("\tinstall-manual %s", manual))
 | ||
| 	}
 | ||
| 	
 | ||
| 	// install-desktop команды
 | ||
| 	for _, desktop := range r.DesktopFiles {
 | ||
| 		commands = append(commands, fmt.Sprintf("\tinstall-desktop %s", desktop))
 | ||
| 	}
 | ||
| 	
 | ||
| 	// install-systemd команды
 | ||
| 	for _, service := range r.ServiceFiles {
 | ||
| 		if strings.Contains(service, "user") {
 | ||
| 			commands = append(commands, fmt.Sprintf("\tinstall-systemd-user %s", service))
 | ||
| 		} else {
 | ||
| 			commands = append(commands, fmt.Sprintf("\tinstall-systemd %s", service))
 | ||
| 		}
 | ||
| 	}
 | ||
| 	
 | ||
| 	// install-completion команды
 | ||
| 	for shell, file := range r.CompletionFiles {
 | ||
| 		switch shell {
 | ||
| 		case "bash":
 | ||
| 			commands = append(commands, fmt.Sprintf("\tinstall-completion bash %s < %s", r.Name, file))
 | ||
| 		case "zsh":
 | ||
| 			commands = append(commands, fmt.Sprintf("\tinstall-completion zsh %s < %s", r.Name, file))
 | ||
| 		case "fish":
 | ||
| 			commands = append(commands, fmt.Sprintf("\t%s completion fish | install-completion fish %s", r.Name, r.Name))
 | ||
| 		}
 | ||
| 	}
 | ||
| 	
 | ||
| 	if len(commands) == 0 {
 | ||
| 		return "\t# TODO: Добавьте команды установки файлов"
 | ||
| 	}
 | ||
| 	
 | ||
| 	return strings.Join(commands, "\n")
 | ||
| }
 | ||
| 
 | ||
| // fetchPKGBUILD загружает PKGBUILD файл для пакета
 | ||
| func fetchPKGBUILD(packageBase string) (string, error) {
 | ||
| 	// URL для raw PKGBUILD
 | ||
| 	pkgbuildURL := fmt.Sprintf("https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=%s", packageBase)
 | ||
| 	
 | ||
| 	res, err := http.Get(pkgbuildURL)
 | ||
| 	if err != nil {
 | ||
| 		return "", fmt.Errorf("failed to fetch PKGBUILD: %w", err)
 | ||
| 	}
 | ||
| 	defer res.Body.Close()
 | ||
| 	
 | ||
| 	if res.StatusCode != 200 {
 | ||
| 		return "", fmt.Errorf("failed to fetch PKGBUILD: status %s", res.Status)
 | ||
| 	}
 | ||
| 	
 | ||
| 	data, err := io.ReadAll(res.Body)
 | ||
| 	if err != nil {
 | ||
| 		return "", fmt.Errorf("failed to read PKGBUILD: %w", err)
 | ||
| 	}
 | ||
| 	
 | ||
| 	return string(data), nil
 | ||
| }
 | ||
| 
 | ||
| // parseSources извлекает источники из PKGBUILD
 | ||
| func parseSources(pkgbuild string) []string {
 | ||
| 	var sources []string
 | ||
| 	
 | ||
| 	// Регулярное выражение для поиска массива source
 | ||
| 	// Поддерживает как однострочные, так и многострочные определения
 | ||
| 	sourceRegex := regexp.MustCompile(`(?ms)source=\((.*?)\)`)
 | ||
| 	matches := sourceRegex.FindStringSubmatch(pkgbuild)
 | ||
| 	
 | ||
| 	if len(matches) > 1 {
 | ||
| 		// Извлекаем содержимое массива source
 | ||
| 		sourceContent := matches[1]
 | ||
| 		
 | ||
| 		// Разбираем элементы массива
 | ||
| 		// Учитываем кавычки и переносы строк
 | ||
| 		elemRegex := regexp.MustCompile(`['"]([^'"]+)['"]`)
 | ||
| 		elements := elemRegex.FindAllStringSubmatch(sourceContent, -1)
 | ||
| 		
 | ||
| 		for _, elem := range elements {
 | ||
| 			if len(elem) > 1 {
 | ||
| 				source := elem[1]
 | ||
| 				// Заменяем переменные версии
 | ||
| 				source = strings.ReplaceAll(source, "$pkgver", "${version}")
 | ||
| 				source = strings.ReplaceAll(source, "${pkgver}", "${version}")
 | ||
| 				source = strings.ReplaceAll(source, "$pkgname", "${name}")
 | ||
| 				source = strings.ReplaceAll(source, "${pkgname}", "${name}")
 | ||
| 				// Обрабатываем другие переменные (упрощенно)
 | ||
| 				source = strings.ReplaceAll(source, "$_commit", "${_commit}")
 | ||
| 				sources = append(sources, source)
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| 	
 | ||
| 	// Если источники не найдены в source=(), проверяем source_x86_64 и другие архитектуры
 | ||
| 	if len(sources) == 0 {
 | ||
| 		archSourceRegex := regexp.MustCompile(`(?ms)source_(?:x86_64|aarch64)=\((.*?)\)`)
 | ||
| 		matches = archSourceRegex.FindStringSubmatch(pkgbuild)
 | ||
| 		if len(matches) > 1 {
 | ||
| 			sourceContent := matches[1]
 | ||
| 			elemRegex := regexp.MustCompile(`['"]([^'"]+)['"]`)
 | ||
| 			elements := elemRegex.FindAllStringSubmatch(sourceContent, -1)
 | ||
| 			
 | ||
| 			for _, elem := range elements {
 | ||
| 				if len(elem) > 1 {
 | ||
| 					source := elem[1]
 | ||
| 					source = strings.ReplaceAll(source, "$pkgver", "${version}")
 | ||
| 					source = strings.ReplaceAll(source, "${pkgver}", "${version}")
 | ||
| 					source = strings.ReplaceAll(source, "$pkgname", "${name}")
 | ||
| 					source = strings.ReplaceAll(source, "${pkgname}", "${name}")
 | ||
| 					sources = append(sources, source)
 | ||
| 				}
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| 	
 | ||
| 	return sources
 | ||
| }
 | ||
| 
 | ||
| // parseChecksums извлекает контрольные суммы из PKGBUILD
 | ||
| func parseChecksums(pkgbuild string) []string {
 | ||
| 	var checksums []string
 | ||
| 	
 | ||
| 	// Пробуем разные типы контрольных сумм
 | ||
| 	for _, hashType := range []string{"sha256sums", "sha512sums", "sha1sums", "md5sums", "b2sums"} {
 | ||
| 		regex := regexp.MustCompile(fmt.Sprintf(`(?ms)%s=\((.*?)\)`, hashType))
 | ||
| 		matches := regex.FindStringSubmatch(pkgbuild)
 | ||
| 		
 | ||
| 		if len(matches) > 1 {
 | ||
| 			content := matches[1]
 | ||
| 			elemRegex := regexp.MustCompile(`['"]([^'"]+)['"]`)
 | ||
| 			elements := elemRegex.FindAllStringSubmatch(content, -1)
 | ||
| 			
 | ||
| 			for _, elem := range elements {
 | ||
| 				if len(elem) > 1 {
 | ||
| 					checksums = append(checksums, elem[1])
 | ||
| 				}
 | ||
| 			}
 | ||
| 			
 | ||
| 			if len(checksums) > 0 {
 | ||
| 				break // Используем первый найденный тип хешей
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| 	
 | ||
| 	return checksums
 | ||
| }
 | ||
| 
 | ||
| // parseFunctions извлекает функции build(), package() и prepare() из PKGBUILD
 | ||
| func parseFunctions(pkgbuild string) (buildFunc, packageFunc, prepareFunc string) {
 | ||
| 	// Извлекаем функцию build()
 | ||
| 	buildRegex := regexp.MustCompile(`(?ms)^build\(\)\s*\{(.*?)^\}`)
 | ||
| 	if matches := buildRegex.FindStringSubmatch(pkgbuild); len(matches) > 1 {
 | ||
| 		buildFunc = strings.TrimSpace(matches[1])
 | ||
| 	}
 | ||
| 	
 | ||
| 	// Извлекаем функцию package()
 | ||
| 	packageRegex := regexp.MustCompile(`(?ms)^package\(\)\s*\{(.*?)^\}`)
 | ||
| 	if matches := packageRegex.FindStringSubmatch(pkgbuild); len(matches) > 1 {
 | ||
| 		packageFunc = strings.TrimSpace(matches[1])
 | ||
| 	}
 | ||
| 	
 | ||
| 	// Извлекаем функцию prepare()
 | ||
| 	prepareRegex := regexp.MustCompile(`(?ms)^prepare\(\)\s*\{(.*?)^\}`)
 | ||
| 	if matches := prepareRegex.FindStringSubmatch(pkgbuild); len(matches) > 1 {
 | ||
| 		prepareFunc = strings.TrimSpace(matches[1])
 | ||
| 	}
 | ||
| 	
 | ||
| 	return buildFunc, packageFunc, prepareFunc
 | ||
| }
 | ||
| 
 | ||
| // detectInstallableFiles анализирует PKGBUILD и определяет файлы для install-* команд
 | ||
| func detectInstallableFiles(pkg *aurResult, pkgbuild string) {
 | ||
| 	// Инициализируем карту для файлов автодополнения
 | ||
| 	pkg.CompletionFiles = make(map[string]string)
 | ||
| 	
 | ||
| 	// Для простоты, добавляем стандартные файлы для типа пакета
 | ||
| 	switch pkg.PackageType {
 | ||
| 	case "go":
 | ||
| 		pkg.BinaryFiles = append(pkg.BinaryFiles, "./"+pkg.Name)
 | ||
| 	case "rust":
 | ||
| 		pkg.BinaryFiles = append(pkg.BinaryFiles, "./target/release/"+pkg.Name)
 | ||
| 	case "cpp", "meson":
 | ||
| 		pkg.BinaryFiles = append(pkg.BinaryFiles, "./"+pkg.Name) // обычно в корне после сборки
 | ||
| 	case "bin":
 | ||
| 		pkg.BinaryFiles = append(pkg.BinaryFiles, "./"+pkg.Name)
 | ||
| 	default:
 | ||
| 		if pkg.PackageType != "python" && pkg.PackageType != "nodejs" {
 | ||
| 			pkg.BinaryFiles = append(pkg.BinaryFiles, "./"+pkg.Name)
 | ||
| 		}
 | ||
| 	}
 | ||
| 	
 | ||
| 	// Ищем лицензионные файлы для install-license с более точными паттернами
 | ||
| 	licenseRegex := regexp.MustCompile(`(?i)\b(LICENSE|COPYING|COPYRIGHT|LICENCE)(?:\.[a-zA-Z0-9]+)?\b`)
 | ||
| 	licenseMatches := licenseRegex.FindAllString(pkgbuild, -1)
 | ||
| 	for _, match := range licenseMatches {
 | ||
| 		// Фильтруем только реальные файлы лицензий
 | ||
| 		if strings.Contains(strings.ToLower(match), "license") || 
 | ||
| 		   strings.Contains(strings.ToLower(match), "copying") || 
 | ||
| 		   strings.Contains(strings.ToLower(match), "copyright") {
 | ||
| 			if !contains(pkg.LicenseFiles, "./"+match) {
 | ||
| 				pkg.LicenseFiles = append(pkg.LicenseFiles, "./"+match)
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| 	
 | ||
| 	// Если не найдены лицензионные файлы, добавляем стандартные
 | ||
| 	if len(pkg.LicenseFiles) == 0 {
 | ||
| 		pkg.LicenseFiles = append(pkg.LicenseFiles, "LICENSE")
 | ||
| 	}
 | ||
| 	
 | ||
| 	// Ищем man страницы для install-manual с более точными паттернами
 | ||
| 	manRegex := regexp.MustCompile(`\b\w+\.(?:1|2|3|4|5|6|7|8)(?:\.gz)?\b`)
 | ||
| 	manMatches := manRegex.FindAllString(pkgbuild, -1)
 | ||
| 	for _, match := range manMatches {
 | ||
| 		// Проверяем, что это не переменная или часть кода
 | ||
| 		if !strings.Contains(match, "$") && !strings.Contains(match, "{") {
 | ||
| 			if !contains(pkg.ManualFiles, "./"+match) {
 | ||
| 				pkg.ManualFiles = append(pkg.ManualFiles, "./"+match)
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| 	
 | ||
| 	// Ищем desktop файлы для install-desktop
 | ||
| 	desktopRegex := regexp.MustCompile(`[^/\s]*\.desktop`)
 | ||
| 	desktopMatches := desktopRegex.FindAllString(pkgbuild, -1)
 | ||
| 	for _, match := range desktopMatches {
 | ||
| 		if !contains(pkg.DesktopFiles, "./"+match) {
 | ||
| 			pkg.DesktopFiles = append(pkg.DesktopFiles, "./"+match)
 | ||
| 		}
 | ||
| 	}
 | ||
| 	
 | ||
| 	// Ищем systemd сервисы для install-systemd
 | ||
| 	serviceRegex := regexp.MustCompile(`[^/\s]*\.service`)
 | ||
| 	serviceMatches := serviceRegex.FindAllString(pkgbuild, -1)
 | ||
| 	for _, match := range serviceMatches {
 | ||
| 		if !contains(pkg.ServiceFiles, "./"+match) {
 | ||
| 			pkg.ServiceFiles = append(pkg.ServiceFiles, "./"+match)
 | ||
| 		}
 | ||
| 	}
 | ||
| 	
 | ||
| 	// Ищем файлы автодополнения
 | ||
| 	completionPatterns := map[string]string{
 | ||
| 		"bash": `completions?/.*\.bash|bash-completion`,
 | ||
| 		"zsh":  `completions?/.*\.zsh|zsh.*completion`,
 | ||
| 		"fish": `completions?/.*\.fish|fish.*completion`,
 | ||
| 	}
 | ||
| 	
 | ||
| 	for shell, pattern := range completionPatterns {
 | ||
| 		regex := regexp.MustCompile(fmt.Sprintf(`(?i)%s`, pattern))
 | ||
| 		matches := regex.FindAllString(pkgbuild, -1)
 | ||
| 		if len(matches) > 0 {
 | ||
| 			pkg.CompletionFiles[shell] = matches[0]
 | ||
| 		}
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // contains проверяет, содержит ли слайс строк указанную строку
 | ||
| func contains(slice []string, item string) bool {
 | ||
| 	for _, s := range slice {
 | ||
| 		if s == item {
 | ||
| 			return true
 | ||
| 		}
 | ||
| 	}
 | ||
| 	return false
 | ||
| }
 | ||
| 
 | ||
| // detectPackageType определяет тип пакета на основе имени, зависимостей и источников
 | ||
| func detectPackageType(pkg *aurResult, pkgbuild string) {
 | ||
| 	name := strings.ToLower(pkg.Name)
 | ||
| 	
 | ||
| 	// Определяем тип на основе имени пакета
 | ||
| 	switch {
 | ||
| 	case strings.HasPrefix(name, "python") || strings.HasPrefix(name, "python3-"):
 | ||
| 		pkg.PackageType = "python"
 | ||
| 	case strings.Contains(name, "nodejs") || strings.Contains(name, "node-"):
 | ||
| 		pkg.PackageType = "nodejs"
 | ||
| 	case strings.HasSuffix(name, "-bin"):
 | ||
| 		pkg.PackageType = "bin"
 | ||
| 	case strings.HasSuffix(name, "-git"):
 | ||
| 		pkg.PackageType = "git"
 | ||
| 		pkg.HasVersion = true // Git пакеты обычно имеют функцию version()
 | ||
| 	case strings.Contains(name, "rust") || hasRustSources(pkg.Sources):
 | ||
| 		pkg.PackageType = "rust"
 | ||
| 	case strings.Contains(name, "go-") || hasGoSources(pkg.Sources):
 | ||
| 		pkg.PackageType = "go"
 | ||
| 	case strings.Contains(name, "-rust") || strings.Contains(name, "paru") || strings.Contains(name, "cargo-"):
 | ||
| 		pkg.PackageType = "rust"
 | ||
| 	default:
 | ||
| 		// Определяем по зависимостям сборки
 | ||
| 		for _, dep := range pkg.MakeDepends {
 | ||
| 			depLower := strings.ToLower(dep)
 | ||
| 			switch {
 | ||
| 			case strings.Contains(depLower, "meson") || strings.Contains(depLower, "ninja"):
 | ||
| 				pkg.PackageType = "meson"
 | ||
| 			case strings.Contains(depLower, "cmake") || strings.Contains(depLower, "gcc") || strings.Contains(depLower, "clang"):
 | ||
| 				pkg.PackageType = "cpp"
 | ||
| 			case strings.Contains(depLower, "python"):
 | ||
| 				pkg.PackageType = "python"
 | ||
| 			case strings.Contains(depLower, "go"):
 | ||
| 				pkg.PackageType = "go"
 | ||
| 			case strings.Contains(depLower, "rust") || strings.Contains(depLower, "cargo"):
 | ||
| 				pkg.PackageType = "rust"
 | ||
| 			case strings.Contains(depLower, "npm") || strings.Contains(depLower, "nodejs"):
 | ||
| 				pkg.PackageType = "nodejs"
 | ||
| 			}
 | ||
| 			if pkg.PackageType != "" {
 | ||
| 				break
 | ||
| 			}
 | ||
| 		}
 | ||
| 	}
 | ||
| 	
 | ||
| 	// Определяем архитектуры на основе типа пакета
 | ||
| 	if pkg.PackageType == "bin" {
 | ||
| 		pkg.Architectures = []string{"amd64"} // Бинарные пакеты обычно специфичны для архитектуры
 | ||
| 	} else {
 | ||
| 		pkg.Architectures = []string{"all"} // Исходный код собирается для любой архитектуры
 | ||
| 	}
 | ||
| 	
 | ||
| 	// Определяем наличие desktop файлов
 | ||
| 	pkg.HasDesktop = strings.Contains(pkgbuild, ".desktop") || 
 | ||
| 		strings.Contains(pkgbuild, "install-desktop") ||
 | ||
| 		strings.Contains(pkgbuild, "xdg-desktop")
 | ||
| 	
 | ||
| 	// Определяем наличие systemd сервисов
 | ||
| 	pkg.HasSystemd = strings.Contains(pkgbuild, ".service") ||
 | ||
| 		strings.Contains(pkgbuild, "systemctl") ||
 | ||
| 		strings.Contains(pkgbuild, "install-systemd")
 | ||
| 	
 | ||
| 	// Определяем наличие функции version() для -git пакетов
 | ||
| 	pkg.HasVersion = strings.Contains(pkgbuild, "pkgver()") || 
 | ||
| 		(strings.HasSuffix(name, "-git") && strings.Contains(pkgbuild, "git describe"))
 | ||
| 	
 | ||
| 	// Определяем наличие патчей
 | ||
| 	pkg.HasPatches = strings.Contains(pkgbuild, "patch ") || 
 | ||
| 		strings.Contains(pkgbuild, ".patch") ||
 | ||
| 		strings.Contains(pkgbuild, ".diff")
 | ||
| 	
 | ||
| 	// Определяем дополнительные скрипты
 | ||
| 	if strings.Contains(pkgbuild, "post_install") {
 | ||
| 		pkg.HasScripts = append(pkg.HasScripts, "postinstall")
 | ||
| 	}
 | ||
| 	if strings.Contains(pkgbuild, "pre_remove") || strings.Contains(pkgbuild, "post_remove") {
 | ||
| 		pkg.HasScripts = append(pkg.HasScripts, "postremove")
 | ||
| 	}
 | ||
| }
 | ||
| 
 | ||
| // hasRustSources проверяет, содержат ли источники Rust проекты
 | ||
| func hasRustSources(sources []string) bool {
 | ||
| 	for _, src := range sources {
 | ||
| 		if strings.Contains(src, "crates.io") || strings.Contains(src, "Cargo.toml") {
 | ||
| 			return true
 | ||
| 		}
 | ||
| 	}
 | ||
| 	return false
 | ||
| }
 | ||
| 
 | ||
| // hasGoSources проверяет, содержат ли источники Go проекты
 | ||
| func hasGoSources(sources []string) bool {
 | ||
| 	for _, src := range sources {
 | ||
| 		if strings.Contains(src, "github.com") && strings.Contains(src, "/go") {
 | ||
| 			return true
 | ||
| 		}
 | ||
| 	}
 | ||
| 	return false
 | ||
| }
 | ||
| 
 | ||
| // AUR генерирует шаблон alr.sh на основе пакета из AUR
 | ||
| func AUR(w io.Writer, opts AUROptions) error {
 | ||
| 	// Создаем шаблон с функциями
 | ||
| 	tmpl, err := template.New("aur").
 | ||
| 		Funcs(funcs).
 | ||
| 		Parse(aurTmpl)
 | ||
| 	if err != nil {
 | ||
| 		return err
 | ||
| 	}
 | ||
| 
 | ||
| 	// Формируем URL запроса к AUR API
 | ||
| 	apiURL := "https://aur.archlinux.org/rpc/v5/info"
 | ||
| 	params := url.Values{}
 | ||
| 	params.Add("arg[]", opts.Name)
 | ||
| 	fullURL := fmt.Sprintf("%s?%s", apiURL, params.Encode())
 | ||
| 
 | ||
| 	// Выполняем запрос к AUR API
 | ||
| 	res, err := http.Get(fullURL)
 | ||
| 	if err != nil {
 | ||
| 		return fmt.Errorf("failed to fetch AUR package info: %w", err)
 | ||
| 	}
 | ||
| 	defer res.Body.Close()
 | ||
| 
 | ||
| 	if res.StatusCode != 200 {
 | ||
| 		return fmt.Errorf("AUR API returned status: %s", res.Status)
 | ||
| 	}
 | ||
| 
 | ||
| 	// Декодируем ответ
 | ||
| 	var resp aurAPIResponse
 | ||
| 	err = json.NewDecoder(res.Body).Decode(&resp)
 | ||
| 	if err != nil {
 | ||
| 		return fmt.Errorf("failed to decode AUR response: %w", err)
 | ||
| 	}
 | ||
| 
 | ||
| 	// Проверяем наличие ошибки в ответе
 | ||
| 	if resp.Error != "" {
 | ||
| 		return fmt.Errorf("AUR API error: %s", resp.Error)
 | ||
| 	}
 | ||
| 
 | ||
| 	// Проверяем, что пакет найден
 | ||
| 	if resp.ResultCount == 0 {
 | ||
| 		return fmt.Errorf("package '%s' not found in AUR", opts.Name)
 | ||
| 	}
 | ||
| 
 | ||
| 	// Берем первый результат
 | ||
| 	pkg := resp.Results[0]
 | ||
| 
 | ||
| 	// Если указана версия, проверяем соответствие
 | ||
| 	if opts.Version != "" && pkg.Version != opts.Version {
 | ||
| 		// Предупреждаем, но продолжаем с актуальной версией из AUR
 | ||
| 		fmt.Fprintf(w, "# WARNING: Requested version %s, but AUR has %s\n", opts.Version, pkg.Version)
 | ||
| 	}
 | ||
| 
 | ||
| 	// Загружаем PKGBUILD для получения источников
 | ||
| 	pkgbuild, err := fetchPKGBUILD(pkg.PackageBase)
 | ||
| 	if err != nil {
 | ||
| 		// Если не удалось загрузить PKGBUILD, используем fallback на AUR репозиторий
 | ||
| 		fmt.Fprintf(w, "# WARNING: Could not fetch PKGBUILD: %v\n", err)
 | ||
| 		fmt.Fprintf(w, "# Using AUR repository as source\n")
 | ||
| 		pkg.Sources = []string{fmt.Sprintf("%s::git+%s", pkg.Name, pkg.GitURL())}
 | ||
| 		pkg.Checksums = []string{"SKIP"}
 | ||
| 	} else {
 | ||
| 		// Извлекаем источники из PKGBUILD
 | ||
| 		pkg.Sources = parseSources(pkgbuild)
 | ||
| 		pkg.Checksums = parseChecksums(pkgbuild)
 | ||
| 		pkg.BuildFunc, pkg.PackageFunc, pkg.PrepareFunc = parseFunctions(pkgbuild)
 | ||
| 		
 | ||
| 		// Определяем тип пакета
 | ||
| 		detectPackageType(&pkg, pkgbuild)
 | ||
| 		
 | ||
| 		// Определяем файлы для install-* команд
 | ||
| 		detectInstallableFiles(&pkg, pkgbuild)
 | ||
| 		
 | ||
| 		// Если источники не найдены, используем fallback
 | ||
| 		if len(pkg.Sources) == 0 {
 | ||
| 			fmt.Fprintf(w, "# WARNING: No sources found in PKGBUILD\n")
 | ||
| 			fmt.Fprintf(w, "# Using AUR repository as source\n")
 | ||
| 			pkg.Sources = []string{fmt.Sprintf("%s::git+%s", pkg.Name, pkg.GitURL())}
 | ||
| 			pkg.Checksums = []string{"SKIP"}
 | ||
| 		}
 | ||
| 	}
 | ||
| 
 | ||
| 	// Выполняем шаблон
 | ||
| 	return tmpl.Execute(w, pkg)
 | ||
| } |