// 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 . 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/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 ScriptExecutor interface { ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error) PrepareDirs( ctx context.Context, input *BuildInput, basePkg string, ) error ExecuteSecondPass( ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile, varsOfPackages []*alrsh.Package, repoDeps []string, builtDeps []*BuiltDep, basePkg string, ) ([]*BuiltDep, error) } 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 InstallerExecutor interface { InstallLocal(paths []string, opts *manager.Opts) error Install(pkgs []string, opts *manager.Opts) error Remove(pkgs []string, opts *manager.Opts) 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 *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: "default", 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 if !input.opts.Clean { var remainingVars []*alrsh.Package 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, }) } else { remainingVars = append(remainingVars, vars) } } if len(remainingVars) == 0 { return builtDeps, nil } } 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 for _, d := range depends { if _, found := depNames[d]; !found { 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( 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( GetBuiltPaths(res), &manager.Opts{ NoConfirm: !input.BuildOpts().Interactive, }, ) if err != nil { return err } } 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 { slog.Info(gotext.Get("Installing dependencies")) found, notFound, err := b.repos.FindPkgs(ctx, depends) // Поиск зависимостей if err != nil { return nil, nil, fmt.Errorf("failed FindPkgs: %w", err) } repoDeps = notFound // Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез pkgs := cliutils.FlattenPkgs( ctx, found, "install", input.BuildOpts().Interactive, ) type item struct { pkg *alrsh.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, fmt.Errorf("failed build package from db: %w", err) } buildDeps = append(buildDeps, res...) } } repoDeps = removeDuplicates(repoDeps) buildDeps = removeDuplicates(buildDeps) return buildDeps, repoDeps, 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(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(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(GetBuiltPaths(builtDeps), &manager.Opts{ NoConfirm: !input.BuildOpts().Interactive, }) if err != nil { return nil, err } } if len(repoDeps) > 0 { err = i.installerExecutor.Install(repoDeps, &manager.Opts{ NoConfirm: !input.BuildOpts().Interactive, }) if err != nil { return nil, err } } return builtDeps, nil }