Files
ALR/internal/gen/aur.go
2025-08-26 22:25:03 +03:00

663 lines
24 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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