первичная итерация генератора из aur пакетов
Some checks failed
Update alr-git / changelog (push) Failing after 25s
Some checks failed
Update alr-git / changelog (push) Failing after 25s
This commit is contained in:
23
gen.go
23
gen.go
@@ -61,6 +61,29 @@ func GenCmd() *cli.Command {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "aur",
|
||||||
|
Usage: gotext.Get("Generate a ALR script for an AUR package"),
|
||||||
|
Flags: []cli.Flag{
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "name",
|
||||||
|
Aliases: []string{"n"},
|
||||||
|
Required: true,
|
||||||
|
Usage: gotext.Get("Name of the AUR package"),
|
||||||
|
},
|
||||||
|
&cli.StringFlag{
|
||||||
|
Name: "version",
|
||||||
|
Aliases: []string{"v"},
|
||||||
|
Usage: gotext.Get("Version of the package (optional, uses latest if not specified)"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Action: func(c *cli.Context) error {
|
||||||
|
return gen.AUR(os.Stdout, gen.AUROptions{
|
||||||
|
Name: c.String("name"),
|
||||||
|
Version: c.String("version"),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
668
internal/gen/aur.go
Normal file
668
internal/gen/aur.go
Normal file
@@ -0,0 +1,668 @@
|
|||||||
|
// 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 {
|
||||||
|
if license == "LICENSE" || license == "./LICENSE" {
|
||||||
|
commands = append(commands, fmt.Sprintf("\tinstall-license %s %s/LICENSE", license, r.Name))
|
||||||
|
} else {
|
||||||
|
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}")
|
||||||
|
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)
|
||||||
|
}
|
133
internal/gen/tmpls/aur.tmpl.sh
Normal file
133
internal/gen/tmpls/aur.tmpl.sh
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
# Generated from AUR package: {{.Name}}
|
||||||
|
# Package type: {{.PackageType}}
|
||||||
|
# AUR votes: {{.NumVotes}} | Popularity: {{printf "%.2f" .Popularity}}
|
||||||
|
# Original maintainer: {{.Maintainer}}
|
||||||
|
# Adapted for ALR by automation
|
||||||
|
|
||||||
|
name='{{.Name}}'
|
||||||
|
version='{{.Version}}'
|
||||||
|
release='1'
|
||||||
|
desc='{{.Description}}'
|
||||||
|
{{if ne .Description ""}}desc_ru='{{.Description}}'{{end}}
|
||||||
|
homepage='{{.URL}}'
|
||||||
|
maintainer="Евгений Храмов <xpamych@yandex.ru> (imported from AUR)"
|
||||||
|
{{if ne .Description ""}}maintainer_ru="Евгений Храмов <xpamych@yandex.ru> (импортирован из AUR)"{{end}}
|
||||||
|
architectures=({{.ArchitecturesString}})
|
||||||
|
license=({{.LicenseString}})
|
||||||
|
{{if .Provides}}provides=({{range .Provides}}'{{.}}' {{end}}){{end}}
|
||||||
|
{{if .Conflicts}}conflicts=({{range .Conflicts}}'{{.}}' {{end}}){{end}}
|
||||||
|
{{if .Replaces}}replaces=({{range .Replaces}}'{{.}}' {{end}}){{end}}
|
||||||
|
|
||||||
|
# Базовые зависимости
|
||||||
|
{{if .DependsString}}deps=({{.DependsString}}){{else}}deps=(){{end}}
|
||||||
|
{{if .MakeDependsString}}build_deps=({{.MakeDependsString}}){{else}}build_deps=(){{end}}
|
||||||
|
|
||||||
|
# Зависимости для конкретных дистрибутивов (адаптируйте под нужды пакета)
|
||||||
|
{{if .DependsString}}deps_arch=({{.DependsString}})
|
||||||
|
deps_debian=({{.DependsString}})
|
||||||
|
deps_altlinux=({{.DependsString}})
|
||||||
|
deps_alpine=({{.DependsString}}){{end}}
|
||||||
|
|
||||||
|
{{if and .MakeDependsString (ne .PackageType "bin")}}# Зависимости сборки для конкретных дистрибутивов
|
||||||
|
build_deps_arch=({{.MakeDependsString}})
|
||||||
|
build_deps_debian=({{.MakeDependsString}})
|
||||||
|
build_deps_altlinux=({{.MakeDependsString}})
|
||||||
|
build_deps_alpine=({{.MakeDependsString}}){{end}}
|
||||||
|
|
||||||
|
{{if .OptDependsString}}# Опциональные зависимости
|
||||||
|
opt_deps=(
|
||||||
|
{{.OptDependsString}}
|
||||||
|
){{end}}
|
||||||
|
|
||||||
|
# Источники из PKGBUILD
|
||||||
|
sources=({{range .Sources}}"{{.}}" {{end}})
|
||||||
|
checksums=({{range .Checksums}}'{{.}}' {{end}})
|
||||||
|
|
||||||
|
{{if .HasVersion}}# Функция версии для Git-пакетов
|
||||||
|
version() {
|
||||||
|
cd "$srcdir/{{.Name}}"
|
||||||
|
git-version
|
||||||
|
}
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .ScriptsString}}# Дополнительные скрипты
|
||||||
|
scripts=(
|
||||||
|
{{.ScriptsString}}
|
||||||
|
){{end}}
|
||||||
|
|
||||||
|
{{if or .PrepareFunc .HasPatches}}prepare() {
|
||||||
|
cd "$srcdir"{{if .PrepareFunc}}
|
||||||
|
# Из PKGBUILD:
|
||||||
|
{{.PrepareFunc}}{{else}}
|
||||||
|
# Применение патчей и подготовка исходников
|
||||||
|
# Раскомментируйте и адаптируйте при необходимости:
|
||||||
|
# patch -p1 < "${scriptdir}/fix.patch"{{end}}
|
||||||
|
}{{else}}# prepare() {
|
||||||
|
# cd "$srcdir"
|
||||||
|
# # Применение патчей и подготовка исходников при необходимости
|
||||||
|
# # patch -p1 < "${scriptdir}/fix.patch"
|
||||||
|
# }{{end}}
|
||||||
|
|
||||||
|
{{if ne .PackageType "bin"}}build() {
|
||||||
|
cd "$srcdir"{{if .BuildFunc}}
|
||||||
|
# Из PKGBUILD:
|
||||||
|
{{.BuildFunc}}{{else}}
|
||||||
|
|
||||||
|
# TODO: Адаптируйте команды сборки под конкретный проект ({{.PackageType}})
|
||||||
|
{{if eq .PackageType "meson"}}# Для Meson проектов:
|
||||||
|
meson setup build --prefix=/usr --buildtype=release
|
||||||
|
ninja -C build -j $(nproc){{else if eq .PackageType "cpp"}}# Для C/C++ проектов:
|
||||||
|
mkdir -p build && cd build
|
||||||
|
cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
|
||||||
|
make -j$(nproc){{else if eq .PackageType "go"}}# Для Go проектов:
|
||||||
|
go build -buildmode=pie -trimpath -ldflags "-s -w" -o {{.Name}}{{else if eq .PackageType "python"}}# Для Python проектов:
|
||||||
|
python -m build --wheel --no-isolation{{else if eq .PackageType "nodejs"}}# Для Node.js проектов:
|
||||||
|
npm ci --production
|
||||||
|
npm run build{{else if eq .PackageType "rust"}}# Для Rust проектов:
|
||||||
|
cargo build --release --locked{{else if eq .PackageType "git"}}# Для Git проектов (обычно исходный код):
|
||||||
|
make -j$(nproc){{else}}# Стандартная сборка:
|
||||||
|
make -j$(nproc){{end}}{{end}}
|
||||||
|
}{{else}}# Бинарный пакет - сборка не требуется{{end}}
|
||||||
|
|
||||||
|
package() {
|
||||||
|
cd "$srcdir"{{if .PackageFunc}}
|
||||||
|
# Из PKGBUILD (адаптировано для ALR):
|
||||||
|
{{.PackageFunc}}
|
||||||
|
|
||||||
|
# Автоматически сгенерированные команды установки:
|
||||||
|
{{.GenerateInstallCommands}}{{else}}
|
||||||
|
|
||||||
|
# TODO: Адаптируйте установку файлов под конкретный проект {{.Name}}
|
||||||
|
{{if eq .PackageType "meson"}}# Для Meson проектов:
|
||||||
|
meson install -C build --destdir="$pkgdir"{{else if eq .PackageType "cpp"}}# Для C/C++ проектов:
|
||||||
|
cd build
|
||||||
|
make DESTDIR="$pkgdir" install{{else if eq .PackageType "go"}}# Для Go проектов:
|
||||||
|
# Исполняемый файл уже собран в корне{{else if eq .PackageType "python"}}# Для Python проектов:
|
||||||
|
pip install --root="$pkgdir/" . --no-deps --disable-pip-version-check{{else if eq .PackageType "nodejs"}}# Для Node.js проектов:
|
||||||
|
npm install -g --prefix="$pkgdir/usr" .{{else if eq .PackageType "rust"}}# Для Rust проектов:
|
||||||
|
# Исполняемый файл в target/release/{{else if eq .PackageType "bin"}}# Бинарный пакет:
|
||||||
|
# Файлы уже распакованы{{else}}# Стандартная установка:
|
||||||
|
make DESTDIR="$pkgdir" install{{end}}
|
||||||
|
|
||||||
|
# Автоматически сгенерированные команды установки:
|
||||||
|
{{.GenerateInstallCommands}}{{end}}
|
||||||
|
}
|
@@ -1,19 +1,3 @@
|
|||||||
// 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/>.
|
|
||||||
|
|
||||||
// DO NOT EDIT MANUALLY. This file is generated.
|
// DO NOT EDIT MANUALLY. This file is generated.
|
||||||
package alrsh
|
package alrsh
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user