Files
ALR/internal/build/script_executor.go

414 lines
11 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.
// 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"
"fmt"
"log/slog"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
"github.com/google/shlex"
"github.com/goreleaser/nfpm/v2"
"github.com/leonelquinteros/gotext"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu"
finddeps "gitea.plemya-x.ru/Plemya-x/ALR/internal/build/find_deps"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/handlers"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/helpers"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
)
type LocalScriptExecutor struct {
cfg Config
}
func NewLocalScriptExecutor(cfg Config) *LocalScriptExecutor {
return &LocalScriptExecutor{
cfg,
}
}
func (e *LocalScriptExecutor) ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error) {
return alrsh.ReadFromLocal(scriptPath)
}
func (e *LocalScriptExecutor) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error) {
return sf.ParseBuildVars(ctx, input.info, input.packages)
}
func (e *LocalScriptExecutor) PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error {
dirs, err := getDirs(
e.cfg,
input.script,
basePkg,
)
if err != nil {
return err
}
err = prepareDirs(dirs)
if err != nil {
return err
}
return nil
}
func (e *LocalScriptExecutor) ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *alrsh.ScriptFile,
varsOfPackages []*alrsh.Package,
repoDeps []string,
builtDeps []*BuiltDep,
basePkg string,
) ([]*BuiltDep, error) {
dirs, err := getDirs(e.cfg, sf.Path(), basePkg)
if err != nil {
return nil, err
}
env := createBuildEnvVars(input.info, dirs)
fakeroot := handlers.FakerootExecHandler(2 * time.Second)
runner, err := interp.New(
interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение
interp.StdIO(os.Stdin, os.Stderr, os.Stderr), // Устанавливаем стандартный ввод-вывод
interp.ExecHandlers(func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
return helpers.Helpers.ExecHandler(fakeroot)
}), // Обрабатываем выполнение через fakeroot
)
if err != nil {
return nil, err
}
err = runner.Run(ctx, sf.File())
if err != nil {
return nil, err
}
dec := decoder.New(input.info, runner)
// var builtPaths []string
err = e.ExecuteFunctions(ctx, dirs, dec)
if err != nil {
return nil, err
}
for _, vars := range varsOfPackages {
packageName := ""
if vars.BasePkgName != "" {
packageName = vars.Name
}
// Для каждого подпакета создаём отдельную директорию
pkgDirs, err := getDirsForPackage(e.cfg, sf.Path(), basePkg, packageName)
if err != nil {
return nil, err
}
// Создаём директорию для подпакета
if err := os.MkdirAll(pkgDirs.PkgDir, 0o755); err != nil {
return nil, err
}
// Обновляем переменную окружения $pkgdir для текущего подпакета
setPkgdirCmd := fmt.Sprintf("pkgdir='%s'", pkgDirs.PkgDir)
setPkgdirScript, err := syntax.NewParser().Parse(strings.NewReader(setPkgdirCmd), "")
if err != nil {
return nil, err
}
err = runner.Run(ctx, setPkgdirScript)
if err != nil {
return nil, err
}
pkgFormat := input.pkgFormat
funcOut, err := e.ExecutePackageFunctions(
ctx,
dec,
pkgDirs,
packageName,
)
if err != nil {
return nil, err
}
slog.Info(gotext.Get("Building package metadata"), "name", basePkg)
pkgInfo, err := buildPkgMetadata(
ctx,
input,
vars,
pkgDirs,
append(
repoDeps,
GetBuiltName(builtDeps)...,
),
funcOut.Contents,
)
if err != nil {
return nil, err
}
packager, err := nfpm.Get(pkgFormat) // Получаем упаковщик для формата пакета
if err != nil {
return nil, err
}
pkgName := packager.ConventionalFileName(pkgInfo) // Получаем имя файла пакета
pkgPath := filepath.Join(pkgDirs.BaseDir, pkgName) // Определяем путь к пакету
slog.Info(gotext.Get("Creating package file"), "path", pkgPath, "name", pkgName)
pkgFile, err := os.Create(pkgPath)
if err != nil {
slog.Error(gotext.Get("Failed to create package file"), "path", pkgPath, "error", err)
return nil, err
}
defer pkgFile.Close()
slog.Info(gotext.Get("Packaging with nfpm"), "format", pkgFormat)
err = packager.Package(pkgInfo, pkgFile)
if err != nil {
slog.Error(gotext.Get("Failed to create package"), "path", pkgPath, "error", err)
return nil, err
}
slog.Info(gotext.Get("Package created successfully"), "path", pkgPath)
// Проверяем, что файл действительно существует
if _, err := os.Stat(pkgPath); err != nil {
slog.Error(gotext.Get("Package file not found after creation"), "path", pkgPath, "error", err)
return nil, err
}
slog.Info(gotext.Get("Package file verified to exist"), "path", pkgPath)
builtDeps = append(builtDeps, &BuiltDep{
Name: vars.Name,
Path: pkgPath,
})
}
return builtDeps, nil
}
func buildPkgMetadata(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
RepositoryProvider
},
vars *alrsh.Package,
dirs types.Directories,
deps []string,
preferedContents *[]string,
) (*nfpm.Info, error) {
pkgInfo := getBasePkgInfo(vars, input)
pkgInfo.Description = vars.Description.Resolved()
pkgInfo.Platform = "linux"
pkgInfo.Homepage = vars.Homepage.Resolved()
pkgInfo.License = strings.Join(vars.Licenses, ", ")
pkgInfo.Maintainer = vars.Maintainer.Resolved()
pkgFormat := input.PkgFormat()
info := input.OSRelease()
// Для RPM на multilib-системах квалифицируем автоконфликт архитектурой (ISA),
// чтобы не удалять пакеты другой архитектуры. Например, установка
// libdrm+alr.x86_64 не должна конфликтовать с libdrm.i686.
autoConflictName := vars.Name
if pkgFormat == "rpm" {
if isa := goArchToRPMISA(cpu.Arch()); isa != "" {
autoConflictName = fmt.Sprintf("%s(%s)", vars.Name, isa)
}
}
pkgInfo.Overridables = nfpm.Overridables{
Conflicts: append(vars.Conflicts, autoConflictName),
Replaces: vars.Replaces,
Provides: append(vars.Provides, vars.Name),
Depends: deps,
}
pkgInfo.Section = vars.Group.Resolved()
if pkgFormat == "apk" {
// Alpine отказывается устанавливать пакеты, которые предоставляют сами себя, поэтому удаляем такие элементы
pkgInfo.Overridables.Provides = slices.DeleteFunc(pkgInfo.Overridables.Provides, func(s string) bool {
return s == pkgInfo.Name
})
}
if pkgFormat == "rpm" {
pkgInfo.RPM.Group = vars.Group.Resolved()
if vars.Summary.Resolved() != "" {
pkgInfo.RPM.Summary = vars.Summary.Resolved()
} else {
lines := strings.SplitN(vars.Description.Resolved(), "\n", 2)
pkgInfo.RPM.Summary = lines[0]
}
}
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
}
normalizeContents(contents)
if vars.FireJailed.Resolved() {
contents, err = applyFirejailIntegration(vars, dirs, contents)
if err != nil {
return nil, err
}
}
pkgInfo.Overridables.Contents = contents
if len(vars.AutoProv.Resolved()) == 1 && decoder.IsTruthy(vars.AutoProv.Resolved()[0]) {
f := finddeps.New(info, pkgFormat)
err = f.FindProvides(ctx, pkgInfo, dirs, vars.AutoProvSkipList.Resolved())
if err != nil {
return nil, err
}
}
if len(vars.AutoReq.Resolved()) == 1 && decoder.IsTruthy(vars.AutoReq.Resolved()[0]) {
f := finddeps.New(info, pkgFormat)
err = f.FindRequires(ctx, pkgInfo, dirs, vars.AutoReqSkipList.Resolved())
if err != nil {
return nil, err
}
}
return pkgInfo, nil
}
func (e *LocalScriptExecutor) ExecuteFunctions(ctx context.Context, dirs types.Directories, dec *decoder.Decoder) error {
prepare, ok := dec.GetFunc("prepare")
if ok {
slog.Info(gotext.Get("Executing prepare()"))
err := prepare(ctx, interp.Dir(dirs.SrcDir))
if err != nil {
return err
}
}
build, ok := dec.GetFunc("build")
if ok {
slog.Info(gotext.Get("Executing build()"))
err := build(ctx, interp.Dir(dirs.SrcDir))
if err != nil {
return err
}
}
return nil
}
func (e *LocalScriptExecutor) ExecutePackageFunctions(
ctx context.Context,
dec *decoder.Decoder,
dirs types.Directories,
packageName string,
) (*FunctionsOutput, error) {
output := &FunctionsOutput{}
var packageFuncName string
var filesFuncName string
if packageName == "" {
packageFuncName = "package"
filesFuncName = "files"
} else {
packageFuncName = fmt.Sprintf("package_%s", packageName)
filesFuncName = fmt.Sprintf("files_%s", packageName)
}
packageFn, ok := dec.GetFunc(packageFuncName)
if ok {
slog.Info(gotext.Get("Executing %s()", packageFuncName))
err := packageFn(ctx, interp.Dir(dirs.SrcDir))
if err != nil {
return nil, err
}
}
files, ok := dec.GetFuncP(filesFuncName, func(ctx context.Context, s *interp.Runner) error {
// It should be done via interp.RunnerOption,
// but due to the issues below, it cannot be done.
// - https://github.com/mvdan/sh/issues/962
// - https://github.com/mvdan/sh/issues/1125
script, err := syntax.NewParser().Parse(strings.NewReader("cd $pkgdir && shopt -s globstar"), "")
if err != nil {
return err
}
return s.Run(ctx, script)
})
if ok {
slog.Info(gotext.Get("Executing %s()", filesFuncName))
buf := &bytes.Buffer{}
err := files(
ctx,
interp.Dir(dirs.PkgDir),
interp.StdIO(os.Stdin, buf, os.Stderr),
)
if err != nil {
return nil, err
}
contents, err := shlex.Split(buf.String())
if err != nil {
return nil, err
}
output.Contents = &contents
}
return output, nil
}