ALR/pkg/build/build.go

699 lines
15 KiB
Go

// 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 Евгений Храмов.
//
// 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 (
"bytes"
"context"
"encoding/gob"
"errors"
"log/slog"
"github.com/leonelquinteros/gotext"
"mvdan.cc/sh/v3/syntax"
"mvdan.cc/sh/v3/syntax/typedjson"
"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/db"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
)
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 ScriptFile struct {
File *syntax.File
Path string
}
func (s *ScriptFile) GobEncode() ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(s.Path); err != nil {
return nil, err
}
var fileBuf bytes.Buffer
if err := typedjson.Encode(&fileBuf, s.File); err != nil {
return nil, err
}
fileData := fileBuf.Bytes()
if err := enc.Encode(fileData); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (s *ScriptFile) GobDecode(data []byte) error {
buf := bytes.NewBuffer(data)
dec := gob.NewDecoder(buf)
if err := dec.Decode(&s.Path); err != nil {
return err
}
var fileData []byte
if err := dec.Decode(&fileData); err != nil {
return err
}
fileReader := bytes.NewReader(fileData)
file, err := typedjson.Decode(fileReader)
if err != nil {
return err
}
s.File = file.(*syntax.File)
return nil
}
type BuildResult struct {
PackagePaths []string
PackageNames []string
}
type PackageFinder interface {
FindPkgs(ctx context.Context, pkgs []string) (map[string][]db.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 *db.Package) *ScriptInfo
}
type ScriptExecutor interface {
ReadScript(ctx context.Context, scriptPath string) (*ScriptFile, error)
ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *ScriptFile) (string, []*types.BuildVars, error)
PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error
ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *ScriptFile,
varsOfPackages []*types.BuildVars,
repoDeps []string,
builtNames []string,
basePkg string,
) (*SecondPassResult, error)
}
type CacheExecutor interface {
CheckForBuiltPackage(ctx context.Context, input *BuildInput, vars *types.BuildVars) (string, bool, error)
}
type ScriptViewerExecutor interface {
ViewScript(ctx context.Context, input *BuildInput, sf *ScriptFile, basePkg string) error
}
type CheckerExecutor interface {
PerformChecks(
ctx context.Context,
input *BuildInput,
vars *types.BuildVars,
) (bool, error)
}
type InstallerExecutor interface {
InstallLocal(paths []string) error
Install(pkgs []string) error
RemoveAlreadyInstalled(pkgs []string) ([]string, 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 *db.Package
Packages []string
}
type BuildPackageFromScriptArgs struct {
BuildArgs
Script string
Packages []string
}
func (b *Builder) BuildPackageFromDb(
ctx context.Context,
args *BuildPackageFromDbArgs,
) (*BuildResult, 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,
) (*BuildResult, error) {
return b.BuildPackage(ctx, &BuildInput{
script: args.Script,
repository: "default",
packages: args.Packages,
pkgFormat: args.PkgFormat(),
opts: args.Opts,
info: args.Info,
})
}
func (b *Builder) BuildPackage(
ctx context.Context,
input *BuildInput,
) (*BuildResult, error) {
scriptPath := input.script
slog.Debug("ReadScript")
sf, err := b.scriptExecutor.ReadScript(ctx, scriptPath)
if err != nil {
return nil, err
}
slog.Debug("ExecuteFirstPass")
basePkg, varsOfPackages, err := b.scriptExecutor.ExecuteFirstPass(ctx, input, sf)
if err != nil {
return nil, err
}
builtPaths := make([]string, 0)
if !input.opts.Clean {
var remainingVars []*types.BuildVars
for _, vars := range varsOfPackages {
builtPkgPath, ok, err := b.cacheExecutor.CheckForBuiltPackage(ctx, input, vars)
if err != nil {
return nil, err
}
if ok {
builtPaths = append(builtPaths, builtPkgPath)
} else {
remainingVars = append(remainingVars, vars)
}
}
if len(remainingVars) == 0 {
return &BuildResult{builtPaths, nil}, nil
}
}
slog.Debug("ViewScript")
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...)
optDepends = append(optDepends, vars.OptDepends...)
depends = append(depends, vars.Depends...)
sources = append(sources, vars.Sources...)
checksums = append(checksums, vars.Checksums...)
}
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")
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
}
slog.Debug("BuildALRDeps")
_, builtNames, repoDeps, err := b.BuildALRDeps(ctx, input, depends)
if err != nil {
return nil, err
}
slog.Debug("PrepareDirs")
err = b.scriptExecutor.PrepareDirs(ctx, input, basePkg)
if err != nil {
return nil, err
}
// builtPaths = append(builtPaths, newBuildPaths...)
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
}
slog.Debug("ExecuteSecondPass")
res, err := b.scriptExecutor.ExecuteSecondPass(
ctx,
input,
sf,
varsOfPackages,
repoDeps,
builtNames,
basePkg,
)
if err != nil {
return nil, err
}
pkgPaths := removeDuplicates(res.BuiltPaths)
pkgNames := removeDuplicates(res.BuiltNames)
return &BuildResult{
PackagePaths: pkgPaths,
PackageNames: pkgNames,
}, nil
}
type InstallPkgsArgs struct {
BuildArgs
AlrPkgs []db.Package
NativePkgs []string
}
func (b *Builder) InstallALRPackages(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
alrPkgs []db.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(res.PackagePaths)
if err != nil {
return err
}
}
return nil
}
func (b *Builder) BuildALRDeps(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
depends []string,
) (builtPaths, builtNames, repoDeps []string, err error) {
if len(depends) > 0 {
slog.Info(gotext.Get("Installing dependencies"))
found, notFound, err := b.repos.FindPkgs(ctx, depends) // Поиск зависимостей
if err != nil {
return nil, nil, nil, err
}
repoDeps = notFound
// Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез
pkgs := cliutils.FlattenPkgs(
ctx,
found,
"install",
input.BuildOpts().Interactive,
)
type item struct {
pkg *db.Package
packages []string
}
pkgsMap := make(map[string]*item)
for _, pkg := range pkgs {
name := pkg.BasePkgName
if name == "" {
name = pkg.Name
}
if pkgsMap[name] == nil {
pkgsMap[name] = &item{
pkg: &pkg,
}
}
pkgsMap[name].packages = append(
pkgsMap[name].packages,
pkg.Name,
)
}
for basePkgName := range pkgsMap {
pkg := pkgsMap[basePkgName].pkg
res, err := b.BuildPackageFromDb(
ctx,
&BuildPackageFromDbArgs{
Package: pkg,
Packages: pkgsMap[basePkgName].packages,
BuildArgs: BuildArgs{
Opts: input.BuildOpts(),
Info: input.OSRelease(),
PkgFormat_: input.PkgFormat(),
},
},
)
if err != nil {
return nil, nil, nil, err
}
builtPaths = append(builtPaths, res.PackagePaths...)
builtNames = append(builtNames, res.PackageNames...)
}
}
// Удаляем возможные дубликаты, которые могут быть введены, если
// несколько зависимостей зависят от одних и тех же пакетов.
repoDeps = removeDuplicates(repoDeps)
builtPaths = removeDuplicates(builtPaths)
builtNames = removeDuplicates(builtNames)
return builtPaths, builtNames, repoDeps, nil
}
func (i *Builder) installBuildDeps(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
pkgs []string,
) error {
if len(pkgs) > 0 {
deps, err := i.installerExecutor.RemoveAlreadyInstalled(pkgs)
if err != nil {
return err
}
err = i.InstallPkgs(ctx, input, deps) // Устанавливаем выбранные пакеты
if err != nil {
return err
}
}
return nil
}
func (i *Builder) installOptDeps(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
pkgs []string,
) error {
optDeps, err := i.installerExecutor.RemoveAlreadyInstalled(pkgs)
if err != nil {
return err
}
if len(optDeps) > 0 {
optDeps, err := cliutils.ChooseOptDepends(
ctx,
optDeps,
"install",
input.BuildOpts().Interactive,
) // Пользователя просят выбрать опциональные зависимости
if err != nil {
return err
}
if len(optDeps) == 0 {
return nil
}
err = i.InstallPkgs(ctx, input, optDeps) // Устанавливаем выбранные пакеты
if err != nil {
return err
}
}
return nil
}
func (i *Builder) InstallPkgs(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
pkgs []string,
) error {
builtPaths, _, repoDeps, err := i.BuildALRDeps(ctx, input, pkgs)
if err != nil {
return err
}
if len(builtPaths) > 0 {
err = i.installerExecutor.InstallLocal(builtPaths)
if err != nil {
return err
}
}
if len(repoDeps) > 0 {
err = i.installerExecutor.Install(repoDeps)
if err != nil {
return err
}
}
return nil
}