424 lines
12 KiB
Go
424 lines
12 KiB
Go
|
// ALR - Any Linux Repository
|
|||
|
// Copyright (C) 2025 Евгений Храмов
|
|||
|
//
|
|||
|
// 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 build
|
|||
|
|
|||
|
import (
|
|||
|
"context"
|
|||
|
"io"
|
|||
|
"log/slog"
|
|||
|
"os"
|
|||
|
"path/filepath"
|
|||
|
"runtime"
|
|||
|
"slices"
|
|||
|
"strconv"
|
|||
|
"strings"
|
|||
|
|
|||
|
// Импортируем пакеты для поддержки различных форматов пакетов (APK, DEB, RPM и ARCH).
|
|||
|
|
|||
|
_ "github.com/goreleaser/nfpm/v2/apk"
|
|||
|
_ "github.com/goreleaser/nfpm/v2/arch"
|
|||
|
_ "github.com/goreleaser/nfpm/v2/deb"
|
|||
|
_ "github.com/goreleaser/nfpm/v2/rpm"
|
|||
|
"github.com/leonelquinteros/gotext"
|
|||
|
"mvdan.cc/sh/v3/syntax"
|
|||
|
|
|||
|
"github.com/goreleaser/nfpm/v2"
|
|||
|
"github.com/goreleaser/nfpm/v2/files"
|
|||
|
|
|||
|
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu"
|
|||
|
"gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
|
|||
|
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
|
|||
|
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder"
|
|||
|
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
|
|||
|
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
|
|||
|
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
|
|||
|
)
|
|||
|
|
|||
|
// Функция readScript анализирует скрипт сборки с использованием встроенной реализации bash
|
|||
|
func readScript(script string) (*syntax.File, error) {
|
|||
|
fl, err := os.Open(script) // Открываем файл скрипта
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
defer fl.Close() // Закрываем файл после выполнения
|
|||
|
|
|||
|
file, err := syntax.NewParser().Parse(fl, "alr.sh") // Парсим скрипт с помощью синтаксического анализатора
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
|
|||
|
return file, nil // Возвращаем синтаксическое дерево
|
|||
|
}
|
|||
|
|
|||
|
// Функция prepareDirs подготавливает директории для сборки.
|
|||
|
func prepareDirs(dirs types.Directories) error {
|
|||
|
err := os.RemoveAll(dirs.BaseDir) // Удаляем базовую директорию, если она существует
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
err = os.MkdirAll(dirs.SrcDir, 0o755) // Создаем директорию для источников
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return os.MkdirAll(dirs.PkgDir, 0o755) // Создаем директорию для пакетов
|
|||
|
}
|
|||
|
|
|||
|
// Функция buildPkgMetadata создает метаданные для пакета, который будет собран.
|
|||
|
func buildPkgMetadata(
|
|||
|
ctx context.Context,
|
|||
|
vars *types.BuildVars,
|
|||
|
dirs types.Directories,
|
|||
|
pkgFormat string,
|
|||
|
info *distro.OSRelease,
|
|||
|
deps []string,
|
|||
|
preferedContents *[]string,
|
|||
|
) (*nfpm.Info, error) {
|
|||
|
pkgInfo := getBasePkgInfo(vars, info)
|
|||
|
pkgInfo.Description = vars.Description
|
|||
|
pkgInfo.Platform = "linux"
|
|||
|
pkgInfo.Homepage = vars.Homepage
|
|||
|
pkgInfo.License = strings.Join(vars.Licenses, ", ")
|
|||
|
pkgInfo.Maintainer = vars.Maintainer
|
|||
|
pkgInfo.Overridables = nfpm.Overridables{
|
|||
|
Conflicts: vars.Conflicts,
|
|||
|
Replaces: vars.Replaces,
|
|||
|
Provides: vars.Provides,
|
|||
|
Depends: deps,
|
|||
|
}
|
|||
|
|
|||
|
if pkgFormat == "apk" {
|
|||
|
// Alpine отказывается устанавливать пакеты, которые предоставляют сами себя, поэтому удаляем такие элементы
|
|||
|
pkgInfo.Overridables.Provides = slices.DeleteFunc(pkgInfo.Overridables.Provides, func(s string) bool {
|
|||
|
return s == pkgInfo.Name
|
|||
|
})
|
|||
|
}
|
|||
|
|
|||
|
if vars.Epoch != 0 {
|
|||
|
pkgInfo.Epoch = strconv.FormatUint(uint64(vars.Epoch), 10)
|
|||
|
}
|
|||
|
|
|||
|
setScripts(vars, pkgInfo, dirs.ScriptDir)
|
|||
|
|
|||
|
if slices.Contains(vars.Architectures, "all") {
|
|||
|
pkgInfo.Arch = "all"
|
|||
|
}
|
|||
|
|
|||
|
contents, err := buildContents(vars, dirs, preferedContents)
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
pkgInfo.Overridables.Contents = contents
|
|||
|
|
|||
|
if len(vars.AutoProv) == 1 && decoder.IsTruthy(vars.AutoProv[0]) {
|
|||
|
if pkgFormat == "rpm" {
|
|||
|
err = rpmFindProvides(ctx, pkgInfo, dirs)
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
} else {
|
|||
|
slog.Info(gotext.Get("AutoProv is not implemented for this package format, so it's skipped"))
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
if len(vars.AutoReq) == 1 && decoder.IsTruthy(vars.AutoReq[0]) {
|
|||
|
if pkgFormat == "rpm" {
|
|||
|
err = rpmFindRequires(ctx, pkgInfo, dirs)
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
} else {
|
|||
|
slog.Info(gotext.Get("AutoReq is not implemented for this package format, so it's skipped"))
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return pkgInfo, nil
|
|||
|
}
|
|||
|
|
|||
|
// Функция buildContents создает секцию содержимого пакета, которая содержит файлы,
|
|||
|
// которые будут включены в конечный пакет.
|
|||
|
func buildContents(vars *types.BuildVars, dirs types.Directories, preferedContents *[]string) ([]*files.Content, error) {
|
|||
|
contents := []*files.Content{}
|
|||
|
|
|||
|
processPath := func(path, trimmed string, prefered bool) error {
|
|||
|
fi, err := os.Lstat(path)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
|
|||
|
if fi.IsDir() {
|
|||
|
f, err := os.Open(path)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
defer f.Close()
|
|||
|
|
|||
|
if !prefered {
|
|||
|
_, err = f.Readdirnames(1)
|
|||
|
if err != io.EOF {
|
|||
|
return nil
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
contents = append(contents, &files.Content{
|
|||
|
Source: path,
|
|||
|
Destination: trimmed,
|
|||
|
Type: "dir",
|
|||
|
FileInfo: &files.ContentFileInfo{
|
|||
|
MTime: fi.ModTime(),
|
|||
|
},
|
|||
|
})
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
if fi.Mode()&os.ModeSymlink != 0 {
|
|||
|
link, err := os.Readlink(path)
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
link = strings.TrimPrefix(link, dirs.PkgDir)
|
|||
|
|
|||
|
contents = append(contents, &files.Content{
|
|||
|
Source: link,
|
|||
|
Destination: trimmed,
|
|||
|
Type: "symlink",
|
|||
|
FileInfo: &files.ContentFileInfo{
|
|||
|
MTime: fi.ModTime(),
|
|||
|
Mode: fi.Mode(),
|
|||
|
},
|
|||
|
})
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
fileContent := &files.Content{
|
|||
|
Source: path,
|
|||
|
Destination: trimmed,
|
|||
|
FileInfo: &files.ContentFileInfo{
|
|||
|
MTime: fi.ModTime(),
|
|||
|
Mode: fi.Mode(),
|
|||
|
Size: fi.Size(),
|
|||
|
},
|
|||
|
}
|
|||
|
|
|||
|
if slices.Contains(vars.Backup, trimmed) {
|
|||
|
fileContent.Type = "config|noreplace"
|
|||
|
}
|
|||
|
|
|||
|
contents = append(contents, fileContent)
|
|||
|
return nil
|
|||
|
}
|
|||
|
|
|||
|
if preferedContents != nil {
|
|||
|
for _, trimmed := range *preferedContents {
|
|||
|
path := filepath.Join(dirs.PkgDir, trimmed)
|
|||
|
if err := processPath(path, trimmed, true); err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
}
|
|||
|
} else {
|
|||
|
err := filepath.Walk(dirs.PkgDir, func(path string, fi os.FileInfo, err error) error {
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
trimmed := strings.TrimPrefix(path, dirs.PkgDir)
|
|||
|
return processPath(path, trimmed, false)
|
|||
|
})
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return contents, nil
|
|||
|
}
|
|||
|
|
|||
|
// Функция checkForBuiltPackage пытается обнаружить ранее собранный пакет и вернуть его путь
|
|||
|
// и true, если нашла. Если нет, возвратит "", false, nil.
|
|||
|
func checkForBuiltPackage(
|
|||
|
mgr manager.Manager,
|
|||
|
vars *types.BuildVars,
|
|||
|
pkgFormat,
|
|||
|
baseDir string,
|
|||
|
info *distro.OSRelease,
|
|||
|
) (string, bool, error) {
|
|||
|
filename, err := pkgFileName(vars, pkgFormat, info)
|
|||
|
if err != nil {
|
|||
|
return "", false, err
|
|||
|
}
|
|||
|
|
|||
|
pkgPath := filepath.Join(baseDir, filename)
|
|||
|
|
|||
|
_, err = os.Stat(pkgPath)
|
|||
|
if err != nil {
|
|||
|
return "", false, nil
|
|||
|
}
|
|||
|
|
|||
|
return pkgPath, true, nil
|
|||
|
}
|
|||
|
|
|||
|
func getBasePkgInfo(vars *types.BuildVars, info *distro.OSRelease) *nfpm.Info {
|
|||
|
return &nfpm.Info{
|
|||
|
Name: vars.Name,
|
|||
|
Arch: cpu.Arch(),
|
|||
|
Version: vars.Version,
|
|||
|
Release: overrides.ReleasePlatformSpecific(vars.Release, info),
|
|||
|
Epoch: strconv.FormatUint(uint64(vars.Epoch), 10),
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
// pkgFileName returns the filename of the package if it were to be built.
|
|||
|
// This is used to check if the package has already been built.
|
|||
|
func pkgFileName(vars *types.BuildVars, pkgFormat string, info *distro.OSRelease) (string, error) {
|
|||
|
pkgInfo := getBasePkgInfo(vars, info)
|
|||
|
|
|||
|
packager, err := nfpm.Get(pkgFormat)
|
|||
|
if err != nil {
|
|||
|
return "", err
|
|||
|
}
|
|||
|
|
|||
|
return packager.ConventionalFileName(pkgInfo), nil
|
|||
|
}
|
|||
|
|
|||
|
// Функция getPkgFormat возвращает формат пакета из менеджера пакетов,
|
|||
|
// или ALR_PKG_FORMAT, если он установлен.
|
|||
|
func getPkgFormat(mgr manager.Manager) string {
|
|||
|
pkgFormat := mgr.Format()
|
|||
|
if format, ok := os.LookupEnv("ALR_PKG_FORMAT"); ok {
|
|||
|
pkgFormat = format
|
|||
|
}
|
|||
|
return pkgFormat
|
|||
|
}
|
|||
|
|
|||
|
// Функция createBuildEnvVars создает переменные окружения, которые будут установлены
|
|||
|
// в скрипте сборки при его выполнении.
|
|||
|
func createBuildEnvVars(info *distro.OSRelease, dirs types.Directories) []string {
|
|||
|
env := os.Environ()
|
|||
|
|
|||
|
env = append(
|
|||
|
env,
|
|||
|
"DISTRO_NAME="+info.Name,
|
|||
|
"DISTRO_PRETTY_NAME="+info.PrettyName,
|
|||
|
"DISTRO_ID="+info.ID,
|
|||
|
"DISTRO_VERSION_ID="+info.VersionID,
|
|||
|
"DISTRO_ID_LIKE="+strings.Join(info.Like, " "),
|
|||
|
"ARCH="+cpu.Arch(),
|
|||
|
"NCPU="+strconv.Itoa(runtime.NumCPU()),
|
|||
|
)
|
|||
|
|
|||
|
if dirs.ScriptDir != "" {
|
|||
|
env = append(env, "scriptdir="+dirs.ScriptDir)
|
|||
|
}
|
|||
|
|
|||
|
if dirs.PkgDir != "" {
|
|||
|
env = append(env, "pkgdir="+dirs.PkgDir)
|
|||
|
}
|
|||
|
|
|||
|
if dirs.SrcDir != "" {
|
|||
|
env = append(env, "srcdir="+dirs.SrcDir)
|
|||
|
}
|
|||
|
|
|||
|
return env
|
|||
|
}
|
|||
|
|
|||
|
// Функция setScripts добавляет скрипты-перехватчики к метаданным пакета.
|
|||
|
func setScripts(vars *types.BuildVars, info *nfpm.Info, scriptDir string) {
|
|||
|
if vars.Scripts.PreInstall != "" {
|
|||
|
info.Scripts.PreInstall = filepath.Join(scriptDir, vars.Scripts.PreInstall)
|
|||
|
}
|
|||
|
|
|||
|
if vars.Scripts.PostInstall != "" {
|
|||
|
info.Scripts.PostInstall = filepath.Join(scriptDir, vars.Scripts.PostInstall)
|
|||
|
}
|
|||
|
|
|||
|
if vars.Scripts.PreRemove != "" {
|
|||
|
info.Scripts.PreRemove = filepath.Join(scriptDir, vars.Scripts.PreRemove)
|
|||
|
}
|
|||
|
|
|||
|
if vars.Scripts.PostRemove != "" {
|
|||
|
info.Scripts.PostRemove = filepath.Join(scriptDir, vars.Scripts.PostRemove)
|
|||
|
}
|
|||
|
|
|||
|
if vars.Scripts.PreUpgrade != "" {
|
|||
|
info.ArchLinux.Scripts.PreUpgrade = filepath.Join(scriptDir, vars.Scripts.PreUpgrade)
|
|||
|
info.APK.Scripts.PreUpgrade = filepath.Join(scriptDir, vars.Scripts.PreUpgrade)
|
|||
|
}
|
|||
|
|
|||
|
if vars.Scripts.PostUpgrade != "" {
|
|||
|
info.ArchLinux.Scripts.PostUpgrade = filepath.Join(scriptDir, vars.Scripts.PostUpgrade)
|
|||
|
info.APK.Scripts.PostUpgrade = filepath.Join(scriptDir, vars.Scripts.PostUpgrade)
|
|||
|
}
|
|||
|
|
|||
|
if vars.Scripts.PreTrans != "" {
|
|||
|
info.RPM.Scripts.PreTrans = filepath.Join(scriptDir, vars.Scripts.PreTrans)
|
|||
|
}
|
|||
|
|
|||
|
if vars.Scripts.PostTrans != "" {
|
|||
|
info.RPM.Scripts.PostTrans = filepath.Join(scriptDir, vars.Scripts.PostTrans)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
/*
|
|||
|
// Функция setVersion изменяет переменную версии в скрипте runner.
|
|||
|
// Она используется для установки версии на вывод функции version().
|
|||
|
func setVersion(ctx context.Context, r *interp.Runner, to string) error {
|
|||
|
fl, err := syntax.NewParser().Parse(strings.NewReader("version='"+to+"'"), "")
|
|||
|
if err != nil {
|
|||
|
return err
|
|||
|
}
|
|||
|
return r.Run(ctx, fl)
|
|||
|
}
|
|||
|
*/
|
|||
|
// Returns not installed dependencies
|
|||
|
func removeAlreadyInstalled(opts types.BuildOpts, dependencies []string) ([]string, error) {
|
|||
|
filteredPackages := []string{}
|
|||
|
|
|||
|
for _, dep := range dependencies {
|
|||
|
installed, err := opts.Manager.IsInstalled(dep)
|
|||
|
if err != nil {
|
|||
|
return nil, err
|
|||
|
}
|
|||
|
if installed {
|
|||
|
continue
|
|||
|
}
|
|||
|
filteredPackages = append(filteredPackages, dep)
|
|||
|
}
|
|||
|
|
|||
|
return filteredPackages, nil
|
|||
|
}
|
|||
|
|
|||
|
// Функция packageNames возвращает имена всех предоставленных пакетов.
|
|||
|
func packageNames(pkgs []db.Package) []string {
|
|||
|
names := make([]string, len(pkgs))
|
|||
|
for i, p := range pkgs {
|
|||
|
names[i] = p.Name
|
|||
|
}
|
|||
|
return names
|
|||
|
}
|
|||
|
|
|||
|
// Функция removeDuplicates убирает любые дубликаты из предоставленного среза.
|
|||
|
func removeDuplicates(slice []string) []string {
|
|||
|
seen := map[string]struct{}{}
|
|||
|
result := []string{}
|
|||
|
|
|||
|
for _, s := range slice {
|
|||
|
if _, ok := seen[s]; !ok {
|
|||
|
seen[s] = struct{}{}
|
|||
|
result = append(result, s)
|
|||
|
}
|
|||
|
}
|
|||
|
|
|||
|
return result
|
|||
|
}
|