Files
ALR/internal/build/build.go
2025-12-09 11:33:14 +03:00

846 lines
20 KiB
Go
Raw 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 build
import (
"bytes"
"context"
"encoding/gob"
"errors"
"fmt"
"log/slog"
"github.com/leonelquinteros/gotext"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/stats"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
)
type BuildInput struct {
opts *types.BuildOpts
info *distro.OSRelease
pkgFormat string
script string
repository string
packages []string
}
func (bi *BuildInput) GobEncode() ([]byte, error) {
w := new(bytes.Buffer)
encoder := gob.NewEncoder(w)
if err := encoder.Encode(bi.opts); err != nil {
return nil, err
}
if err := encoder.Encode(bi.info); err != nil {
return nil, err
}
if err := encoder.Encode(bi.pkgFormat); err != nil {
return nil, err
}
if err := encoder.Encode(bi.script); err != nil {
return nil, err
}
if err := encoder.Encode(bi.repository); err != nil {
return nil, err
}
if err := encoder.Encode(bi.packages); err != nil {
return nil, err
}
return w.Bytes(), nil
}
func (bi *BuildInput) GobDecode(data []byte) error {
r := bytes.NewBuffer(data)
decoder := gob.NewDecoder(r)
if err := decoder.Decode(&bi.opts); err != nil {
return err
}
if err := decoder.Decode(&bi.info); err != nil {
return err
}
if err := decoder.Decode(&bi.pkgFormat); err != nil {
return err
}
if err := decoder.Decode(&bi.script); err != nil {
return err
}
if err := decoder.Decode(&bi.repository); err != nil {
return err
}
if err := decoder.Decode(&bi.packages); err != nil {
return err
}
return nil
}
func (b *BuildInput) Repository() string {
return b.repository
}
func (b *BuildInput) BuildOpts() *types.BuildOpts {
return b.opts
}
func (b *BuildInput) OSRelease() *distro.OSRelease {
return b.info
}
func (b *BuildInput) PkgFormat() string {
return b.pkgFormat
}
type BuildOptsProvider interface {
BuildOpts() *types.BuildOpts
}
type OsInfoProvider interface {
OSRelease() *distro.OSRelease
}
type PkgFormatProvider interface {
PkgFormat() string
}
type RepositoryProvider interface {
Repository() string
}
// ================================================
type BuiltDep struct {
Name string
Path string
}
func Map[T, R any](items []T, f func(T) R) []R {
res := make([]R, len(items))
for i, item := range items {
res[i] = f(item)
}
return res
}
func GetBuiltPaths(deps []*BuiltDep) []string {
return Map(deps, func(dep *BuiltDep) string {
return dep.Path
})
}
func GetBuiltName(deps []*BuiltDep) []string {
return Map(deps, func(dep *BuiltDep) string {
return dep.Name
})
}
type PackageFinder interface {
FindPkgs(ctx context.Context, pkgs []string) (map[string][]alrsh.Package, []string, error)
}
type Config interface {
GetPaths() *config.Paths
PagerStyle() string
}
type FunctionsOutput struct {
Contents *[]string
}
// EXECUTORS
type ScriptResolverExecutor interface {
ResolveScript(ctx context.Context, pkg *alrsh.Package) *ScriptInfo
}
type CacheExecutor interface {
CheckForBuiltPackage(ctx context.Context, input *BuildInput, vars *alrsh.Package) (string, bool, error)
}
type ScriptViewerExecutor interface {
ViewScript(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile, basePkg string) error
}
type CheckerExecutor interface {
PerformChecks(
ctx context.Context,
input *BuildInput,
vars *alrsh.Package,
) (bool, error)
}
type SourcesInput struct {
Sources []string
Checksums []string
}
type SourceDownloaderExecutor interface {
DownloadSources(
ctx context.Context,
input *BuildInput,
basePkg string,
si SourcesInput,
) error
}
//
func NewBuilder(
scriptResolver ScriptResolverExecutor,
scriptExecutor ScriptExecutor,
cacheExecutor CacheExecutor,
scriptViewerExecutor ScriptViewerExecutor,
checkerExecutor CheckerExecutor,
installerExecutor InstallerExecutor,
sourceExecutor SourceDownloaderExecutor,
) *Builder {
return &Builder{
scriptResolver: scriptResolver,
scriptExecutor: scriptExecutor,
cacheExecutor: cacheExecutor,
scriptViewerExecutor: scriptViewerExecutor,
checkerExecutor: checkerExecutor,
installerExecutor: installerExecutor,
sourceExecutor: sourceExecutor,
}
}
type Builder struct {
scriptResolver ScriptResolverExecutor
scriptExecutor ScriptExecutor
cacheExecutor CacheExecutor
scriptViewerExecutor ScriptViewerExecutor
checkerExecutor CheckerExecutor
installerExecutor InstallerExecutor
sourceExecutor SourceDownloaderExecutor
repos PackageFinder
// mgr manager.Manager
}
type BuildArgs struct {
Opts *types.BuildOpts
Info *distro.OSRelease
PkgFormat_ string
}
func (b *BuildArgs) BuildOpts() *types.BuildOpts {
return b.Opts
}
func (b *BuildArgs) OSRelease() *distro.OSRelease {
return b.Info
}
func (b *BuildArgs) PkgFormat() string {
return b.PkgFormat_
}
type BuildPackageFromDbArgs struct {
BuildArgs
Package *alrsh.Package
Packages []string
}
type BuildPackageFromScriptArgs struct {
BuildArgs
Script string
Packages []string
}
func (b *Builder) BuildPackageFromDb(
ctx context.Context,
args *BuildPackageFromDbArgs,
) ([]*BuiltDep, error) {
scriptInfo := b.scriptResolver.ResolveScript(ctx, args.Package)
return b.BuildPackage(ctx, &BuildInput{
script: scriptInfo.Script,
repository: scriptInfo.Repository,
packages: args.Packages,
pkgFormat: args.PkgFormat(),
opts: args.Opts,
info: args.Info,
})
}
func (b *Builder) BuildPackageFromScript(
ctx context.Context,
args *BuildPackageFromScriptArgs,
) ([]*BuiltDep, error) {
return b.BuildPackage(ctx, &BuildInput{
script: args.Script,
repository: ExtractRepoNameFromPath(args.Script),
packages: args.Packages,
pkgFormat: args.PkgFormat(),
opts: args.Opts,
info: args.Info,
})
}
func (b *Builder) BuildPackage(
ctx context.Context,
input *BuildInput,
) ([]*BuiltDep, error) {
scriptPath := input.script
slog.Debug("ReadScript")
sf, err := b.scriptExecutor.ReadScript(ctx, scriptPath)
if err != nil {
return nil, fmt.Errorf("failed reading script: %w", err)
}
slog.Debug("ExecuteFirstPass")
basePkg, varsOfPackages, err := b.scriptExecutor.ExecuteFirstPass(ctx, input, sf)
if err != nil {
return nil, fmt.Errorf("failed ExecuteFirstPass: %w", err)
}
var builtDeps []*BuiltDep
var remainingVars []*alrsh.Package
if !input.opts.Clean {
for _, vars := range varsOfPackages {
builtPkgPath, ok, err := b.cacheExecutor.CheckForBuiltPackage(ctx, input, vars)
if err != nil {
return nil, err
}
if ok {
builtDeps = append(builtDeps, &BuiltDep{
Path: builtPkgPath,
Name: vars.Name,
})
} else {
remainingVars = append(remainingVars, vars)
}
}
if len(remainingVars) == 0 {
slog.Info(gotext.Get("Using cached package"), "name", basePkg)
return builtDeps, nil
}
// Обновляем varsOfPackages только теми пакетами, которые нужно собрать
varsOfPackages = remainingVars
}
slog.Debug("ViewScript")
slog.Debug("", "varsOfPackages", varsOfPackages[0])
err = b.scriptViewerExecutor.ViewScript(ctx, input, sf, basePkg)
if err != nil {
return nil, err
}
slog.Info(gotext.Get("Building package"), "name", basePkg)
for _, vars := range varsOfPackages {
cont, err := b.checkerExecutor.PerformChecks(ctx, input, vars)
if err != nil {
return nil, err
}
if !cont {
return nil, errors.New("exit...")
}
}
buildDepends := []string{}
optDepends := []string{}
depends := []string{}
sources := []string{}
checksums := []string{}
for _, vars := range varsOfPackages {
buildDepends = append(buildDepends, vars.BuildDepends.Resolved()...)
optDepends = append(optDepends, vars.OptDepends.Resolved()...)
depends = append(depends, vars.Depends.Resolved()...)
sources = append(sources, vars.Sources.Resolved()...)
checksums = append(checksums, vars.Checksums.Resolved()...)
}
buildDepends = removeDuplicates(buildDepends)
optDepends = removeDuplicates(optDepends)
depends = removeDuplicates(depends)
if len(sources) != len(checksums) {
slog.Error(gotext.Get("The checksums array must be the same length as sources"))
return nil, errors.New("exit...")
}
sources, checksums = removeDuplicatesSources(sources, checksums)
slog.Debug("installBuildDeps")
alrBuildDeps, installedBuildDeps, err := b.installBuildDeps(ctx, input, buildDepends)
if err != nil {
return nil, err
}
slog.Debug("installOptDeps")
_, err = b.installOptDeps(ctx, input, optDepends)
if err != nil {
return nil, err
}
depNames := make(map[string]struct{})
for _, dep := range alrBuildDeps {
depNames[dep.Name] = struct{}{}
}
// We filter so as not to re-build what has already been built at the `installBuildDeps` stage.
var filteredDepends []string
// Создаем набор подпакетов текущего мультипакета для исключения циклических зависимостей
currentPackageNames := make(map[string]struct{})
for _, pkg := range input.packages {
currentPackageNames[pkg] = struct{}{}
}
for _, d := range depends {
if _, found := depNames[d]; !found {
// Исключаем зависимости, которые являются подпакетами текущего мультипакета
if _, isCurrentPackage := currentPackageNames[d]; !isCurrentPackage {
filteredDepends = append(filteredDepends, d)
}
}
}
slog.Debug("BuildALRDeps")
newBuiltDeps, repoDeps, err := b.BuildALRDeps(ctx, input, filteredDepends)
if err != nil {
return nil, err
}
slog.Debug("PrepareDirs")
err = b.scriptExecutor.PrepareDirs(ctx, input, basePkg)
if err != nil {
return nil, err
}
slog.Info(gotext.Get("Downloading sources"))
slog.Debug("DownloadSources")
err = b.sourceExecutor.DownloadSources(
ctx,
input,
basePkg,
SourcesInput{
Sources: sources,
Checksums: checksums,
},
)
if err != nil {
return nil, err
}
builtDeps = removeDuplicates(append(builtDeps, newBuiltDeps...))
slog.Debug("ExecuteSecondPass")
res, err := b.scriptExecutor.ExecuteSecondPass(
ctx,
input,
sf,
varsOfPackages,
repoDeps,
builtDeps,
basePkg,
)
if err != nil {
return nil, err
}
builtDeps = removeDuplicates(append(builtDeps, res...))
err = b.removeBuildDeps(ctx, input, installedBuildDeps)
if err != nil {
return nil, err
}
return builtDeps, nil
}
func (b *Builder) removeBuildDeps(ctx context.Context, input interface {
BuildOptsProvider
}, deps []string,
) error {
if len(deps) > 0 {
remove, err := cliutils.YesNoPrompt(ctx, gotext.Get("Would you like to remove the build dependencies?"), input.BuildOpts().Interactive, false)
if err != nil {
return err
}
if remove {
err = b.installerExecutor.Remove(
ctx,
deps,
&manager.Opts{
NoConfirm: !input.BuildOpts().Interactive,
},
)
if err != nil {
return err
}
}
}
return nil
}
type InstallPkgsArgs struct {
BuildArgs
AlrPkgs []alrsh.Package
NativePkgs []string
}
func (b *Builder) InstallALRPackages(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
alrPkgs []alrsh.Package,
) error {
for _, pkg := range alrPkgs {
res, err := b.BuildPackageFromDb(
ctx,
&BuildPackageFromDbArgs{
Package: &pkg,
Packages: []string{},
BuildArgs: BuildArgs{
Opts: input.BuildOpts(),
Info: input.OSRelease(),
PkgFormat_: input.PkgFormat(),
},
},
)
if err != nil {
return err
}
err = b.installerExecutor.InstallLocal(
ctx,
GetBuiltPaths(res),
&manager.Opts{
NoConfirm: !input.BuildOpts().Interactive,
},
)
if err != nil {
return err
}
// Отслеживание установки ALR пакетов
for _, dep := range res {
if stats.ShouldTrackPackage(dep.Name) {
stats.TrackInstallation(ctx, dep.Name, "upgrade")
}
}
}
return nil
}
func (b *Builder) BuildALRDeps(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
depends []string,
) (buildDeps []*BuiltDep, repoDeps []string, err error) {
if len(depends) == 0 {
return nil, nil, nil
}
slog.Info(gotext.Get("Installing dependencies"))
// Шаг 1: Рекурсивно разрешаем ВСЕ зависимости
depTree, systemDeps, err := b.ResolveDependencyTree(ctx, input, depends)
if err != nil {
return nil, nil, fmt.Errorf("failed to resolve dependency tree: %w", err)
}
// Системные зависимости возвращаем как repoDeps
repoDeps = systemDeps
// Шаг 2: Собираем список всех пакетов из дерева для топологической сортировки
allFound := make(map[string][]alrsh.Package)
for baseName, node := range depTree {
allFound[baseName] = []alrsh.Package{*node.Package}
}
// Шаг 3: Топологическая сортировка (от корней к листьям)
sortedPkgs, err := TopologicalSort(depTree, allFound)
if err != nil {
return nil, nil, fmt.Errorf("failed to sort dependencies: %w", err)
}
// Шаг 4: Собираем пакеты в правильном порядке, проверяя кеш
for _, basePkgName := range sortedPkgs {
node := depTree[basePkgName]
if node == nil {
continue
}
pkg := node.Package
// Находим ВСЕ подпакеты с этим BasePkgName
allSubpkgs, err := b.findAllSubpackages(ctx, basePkgName, pkg.Repository)
if err != nil {
return nil, nil, fmt.Errorf("failed to find subpackages for %s: %w", basePkgName, err)
}
// Проверяем кеш для ВСЕХ подпакетов
scriptInfo := b.scriptResolver.ResolveScript(ctx, pkg)
buildInput := &BuildInput{
script: scriptInfo.Script,
repository: scriptInfo.Repository,
packages: allSubpkgs,
pkgFormat: input.PkgFormat(),
opts: input.BuildOpts(),
info: input.OSRelease(),
}
cachedDeps, allInCache, err := b.checkCacheForAllSubpackages(ctx, buildInput, basePkgName, allSubpkgs)
if err != nil {
return nil, nil, err
}
if allInCache {
// Все подпакеты в кеше, используем их
buildDeps = append(buildDeps, cachedDeps...)
continue
}
// Собираем пакет (без рекурсивной сборки зависимостей, так как они уже собраны)
res, err := b.BuildPackageFromDb(
ctx,
&BuildPackageFromDbArgs{
Package: pkg,
Packages: allSubpkgs,
BuildArgs: BuildArgs{
Opts: input.BuildOpts(),
Info: input.OSRelease(),
PkgFormat_: input.PkgFormat(),
},
},
)
if err != nil {
return nil, nil, fmt.Errorf("failed build package from db: %w", err)
}
buildDeps = append(buildDeps, res...)
}
buildDeps = removeDuplicates(buildDeps)
return buildDeps, repoDeps, nil
}
// findAllSubpackages находит все подпакеты для базового пакета
func (b *Builder) findAllSubpackages(ctx context.Context, basePkgName, repository string) ([]string, error) {
// Запрашиваем все пакеты с этим basepkg_name
pkgs, _, err := b.repos.FindPkgs(ctx, []string{basePkgName})
if err != nil {
return nil, err
}
var subpkgs []string
seen := make(map[string]bool)
for _, pkgList := range pkgs {
for _, pkg := range pkgList {
// Проверяем, что это пакет из нужного репозитория
if pkg.Repository == repository {
pkgBase := pkg.BasePkgName
if pkgBase == "" {
pkgBase = pkg.Name
}
// Добавляем только если это пакет с нужным BasePkgName
if pkgBase == basePkgName && !seen[pkg.Name] {
subpkgs = append(subpkgs, pkg.Name)
seen[pkg.Name] = true
}
}
}
}
return subpkgs, nil
}
// checkCacheForAllSubpackages проверяет кеш для всех подпакетов
func (b *Builder) checkCacheForAllSubpackages(
ctx context.Context,
buildInput *BuildInput,
basePkgName string,
subpkgs []string,
) ([]*BuiltDep, bool, error) {
var cachedDeps []*BuiltDep
allInCache := true
// Получаем информацию обо всех подпакетах
pkgsInfo, _, err := b.repos.FindPkgs(ctx, subpkgs)
if err != nil {
return nil, false, fmt.Errorf("failed to find subpackages info: %w", err)
}
for _, pkgName := range subpkgs {
var pkgForCheck *alrsh.Package
// Находим Package для подпакета
if pkgList, ok := pkgsInfo[pkgName]; ok && len(pkgList) > 0 {
pkgForCheck = &pkgList[0]
}
if pkgForCheck != nil {
pkgPath, found, err := b.cacheExecutor.CheckForBuiltPackage(ctx, buildInput, pkgForCheck)
if err != nil {
return nil, false, fmt.Errorf("failed to check cache: %w", err)
}
if found {
slog.Info(gotext.Get("Using cached package"), "name", pkgName, "path", pkgPath)
cachedDeps = append(cachedDeps, &BuiltDep{
Name: pkgName,
Path: pkgPath,
})
} else {
allInCache = false
break
}
}
}
return cachedDeps, allInCache && len(cachedDeps) > 0, nil
}
func (i *Builder) installBuildDeps(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
pkgs []string,
) ([]*BuiltDep, []string, error) {
var builtDeps []*BuiltDep
var deps []string
var err error
if len(pkgs) > 0 {
deps, err = i.installerExecutor.RemoveAlreadyInstalled(ctx, pkgs)
if err != nil {
return nil, nil, err
}
builtDeps, err = i.InstallPkgs(ctx, input, deps) // Устанавливаем выбранные пакеты
if err != nil {
return nil, nil, err
}
}
return builtDeps, deps, nil
}
func (i *Builder) installOptDeps(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
pkgs []string,
) ([]*BuiltDep, error) {
var builtDeps []*BuiltDep
optDeps, err := i.installerExecutor.RemoveAlreadyInstalled(ctx, pkgs)
if err != nil {
return nil, err
}
if len(optDeps) > 0 {
optDeps, err := cliutils.ChooseOptDepends(
ctx,
optDeps,
"install",
input.BuildOpts().Interactive,
) // Пользователя просят выбрать опциональные зависимости
if err != nil {
return nil, err
}
if len(optDeps) == 0 {
return builtDeps, nil
}
builtDeps, err = i.InstallPkgs(ctx, input, optDeps) // Устанавливаем выбранные пакеты
if err != nil {
return nil, err
}
}
return builtDeps, nil
}
func (i *Builder) InstallPkgs(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
pkgs []string,
) ([]*BuiltDep, error) {
builtDeps, repoDeps, err := i.BuildALRDeps(ctx, input, pkgs)
if err != nil {
return nil, err
}
if len(builtDeps) > 0 {
err = i.installerExecutor.InstallLocal(ctx, GetBuiltPaths(builtDeps), &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive,
})
if err != nil {
return nil, err
}
// Отслеживание установки локальных пакетов
for _, dep := range builtDeps {
if stats.ShouldTrackPackage(dep.Name) {
stats.TrackInstallation(ctx, dep.Name, "install")
}
}
}
if len(repoDeps) > 0 {
err = i.installerExecutor.Install(ctx, repoDeps, &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive,
})
if err != nil {
return nil, err
}
// Отслеживание установки пакетов из репозитория
for _, pkg := range repoDeps {
if stats.ShouldTrackPackage(pkg) {
stats.TrackInstallation(ctx, pkg, "install")
}
}
}
return builtDeps, nil
}