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