feat: add support for multiple packages in one alr.sh
This commit is contained in:
		
							
								
								
									
										423
									
								
								pkg/build/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										423
									
								
								pkg/build/utils.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,423 @@ | ||||
| // 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 | ||||
| } | ||||
		Reference in New Issue
	
	Block a user