ALR/pkg/build/build.go

608 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 Евгений Храмов.
//
// 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/hex"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"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/cliutils"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dl"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dlcache"
"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/internal/types"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/repos"
)
type Builder struct {
ctx context.Context
opts types.BuildOpts
info *distro.OSRelease
repos *repos.Repos
config *config.ALRConfig
}
func New(
ctx context.Context,
opts types.BuildOpts,
repos *repos.Repos,
info *distro.OSRelease,
config *config.ALRConfig,
) *Builder {
return &Builder{
ctx: ctx,
opts: opts,
info: info,
repos: repos,
config: config,
}
}
func (b *Builder) BuildPackage(ctx context.Context) ([]string, []string, error) {
fl, err := readScript(b.opts.Script)
if err != nil {
return nil, nil, err
}
// Первый проход предназначен для получения значений переменных и выполняется
// до отображения скрипта, чтобы предотвратить выполнение вредоносного кода.
vars, err := b.executeFirstPass(fl)
if err != nil {
return nil, nil, err
}
dirs := b.getDirs(vars)
// Если флаг opts.Clean не установлен, и пакет уже собран,
// возвращаем его, а не собираем заново.
if !b.opts.Clean {
builtPkgPath, ok, err := checkForBuiltPackage(b.opts.Manager, vars, getPkgFormat(b.opts.Manager), dirs.BaseDir)
if err != nil {
return nil, nil, err
}
if ok {
return []string{builtPkgPath}, nil, err
}
}
// Спрашиваем у пользователя, хочет ли он увидеть скрипт сборки.
err = cliutils.PromptViewScript(
ctx,
b.opts.Script,
vars.Name,
b.config.PagerStyle(ctx),
b.opts.Interactive,
)
if err != nil {
slog.Error(gotext.Get("Failed to prompt user to view build script"), "err", err)
os.Exit(1)
}
slog.Info(gotext.Get("Building package"), "name", vars.Name, "version", vars.Version)
// Второй проход будет использоваться для выполнения реального кода,
// поэтому он не ограничен. Скрипт уже был показан
// пользователю к этому моменту, так что это должно быть безопасно.
dec, err := b.executeSecondPass(ctx, fl, dirs)
if err != nil {
return nil, nil, err
}
// Получаем список установленных пакетов в системе
installed, err := b.opts.Manager.ListInstalled(nil)
if err != nil {
return nil, nil, err
}
cont, err := b.performChecks(ctx, vars, installed) // Выполняем различные проверки
if err != nil {
return nil, nil, err
} else if !cont {
os.Exit(1) // Если проверки не пройдены, выходим из программы
}
// Подготавливаем директории для сборки
err = prepareDirs(dirs)
if err != nil {
return nil, nil, err
}
buildDeps, err := b.installBuildDeps(ctx, vars) // Устанавливаем зависимости для сборки
if err != nil {
return nil, nil, err
}
err = installOptDeps(ctx, b.repos, vars, b.opts) // Устанавливаем опциональные зависимости
if err != nil {
return nil, nil, err
}
builtPaths, builtNames, repoDeps, err := b.buildALRDeps(ctx, vars) // Собираем зависимости
if err != nil {
return nil, nil, err
}
slog.Info(gotext.Get("Downloading sources")) // Записываем в лог загрузку источников
err = b.getSources(ctx, dirs, vars) // Загружаем исходники
if err != nil {
return nil, nil, err
}
funcOut, err := b.executeFunctions(ctx, dec, dirs, vars) // Выполняем специальные функции
if err != nil {
return nil, nil, err
}
slog.Info(gotext.Get("Building package metadata"), "name", vars.Name)
pkgFormat := getPkgFormat(b.opts.Manager) // Получаем формат пакета
pkgInfo, err := buildPkgMetadata(ctx, vars, dirs, pkgFormat, b.info, append(repoDeps, builtNames...), funcOut.Contents) // Собираем метаданные пакета
if err != nil {
return nil, nil, err
}
packager, err := nfpm.Get(pkgFormat) // Получаем упаковщик для формата пакета
if err != nil {
return nil, nil, err
}
pkgName := packager.ConventionalFileName(pkgInfo) // Получаем имя файла пакета
pkgPath := filepath.Join(dirs.BaseDir, pkgName) // Определяем путь к пакету
pkgFile, err := os.Create(pkgPath) // Создаём файл пакета
if err != nil {
return nil, nil, err
}
slog.Info(gotext.Get("Compressing package"), "name", pkgName) // Логгируем сжатие пакета
err = packager.Package(pkgInfo, pkgFile) // Упаковываем пакет
if err != nil {
return nil, nil, err
}
err = b.removeBuildDeps(ctx, buildDeps) // Удаляем зависимости для сборки
if err != nil {
return nil, nil, err
}
// Добавляем путь и имя только что собранного пакета в
// соответствующие срезы
builtPaths = append(builtPaths, pkgPath)
builtNames = append(builtNames, vars.Name)
// Удаляем дубликаты из pkgPaths и pkgNames.
// Дубликаты могут появиться, если несколько зависимостей
// зависят от одних и тех же пакетов.
pkgPaths := removeDuplicates(builtPaths)
pkgNames := removeDuplicates(builtNames)
return pkgPaths, pkgNames, nil // Возвращаем пути и имена пакетов
}
// Функция executeFirstPass выполняет парсированный скрипт в ограниченной среде,
// чтобы извлечь переменные сборки без выполнения реального кода.
func (b *Builder) executeFirstPass(
fl *syntax.File,
) (*types.BuildVars, error) {
scriptDir := filepath.Dir(b.opts.Script) // Получаем директорию скрипта
env := createBuildEnvVars(b.info, types.Directories{ScriptDir: scriptDir}) // Создаём переменные окружения для сборки
runner, err := interp.New(
interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение
interp.StdIO(os.Stdin, os.Stdout, os.Stderr), // Устанавливаем стандартный ввод-вывод
interp.ExecHandler(helpers.Restricted.ExecHandler(handlers.NopExec)), // Ограничиваем выполнение
interp.ReadDirHandler(handlers.RestrictedReadDir(scriptDir)), // Ограничиваем чтение директорий
interp.StatHandler(handlers.RestrictedStat(scriptDir)), // Ограничиваем доступ к статистике файлов
interp.OpenHandler(handlers.RestrictedOpen(scriptDir)), // Ограничиваем открытие файлов
)
if err != nil {
return nil, err
}
err = runner.Run(b.ctx, fl) // Запускаем скрипт
if err != nil {
return nil, err
}
dec := decoder.New(b.info, runner) // Создаём новый декодер
var vars types.BuildVars
if b.opts.Package == "" {
err = dec.DecodeVars(&vars) // Декодируем переменные
if err != nil {
return nil, err
}
return &vars, nil
}
var preVars types.BuildVarsPre
funcName := fmt.Sprintf("meta_%s", b.opts.Package)
meta, ok := dec.GetFuncSub(funcName)
if !ok {
return nil, errors.New("func is missing")
}
r, err := meta(b.ctx)
if err != nil {
return nil, err
}
d := decoder.New(&distro.OSRelease{}, r)
err = d.DecodeVars(&preVars)
if err != nil {
return nil, err
}
vars = preVars.ToBuildVars()
vars.Name = b.opts.Package
return &vars, nil // Возвращаем переменные сборки
}
// Функция getDirs возвращает соответствующие директории для скрипта
func (b *Builder) getDirs(vars *types.BuildVars) types.Directories {
baseDir := filepath.Join(b.config.GetPaths(b.ctx).PkgsDir, vars.Name) // Определяем базовую директорию
return types.Directories{
BaseDir: baseDir,
SrcDir: filepath.Join(baseDir, "src"),
PkgDir: filepath.Join(baseDir, "pkg"),
ScriptDir: filepath.Dir(b.opts.Script),
}
}
// Функция executeSecondPass выполняет скрипт сборки второй раз без каких-либо ограничений. Возвращается декодер,
// который может быть использован для получения функций и переменных из скрипта.
func (b *Builder) executeSecondPass(
ctx context.Context,
fl *syntax.File,
dirs types.Directories,
) (*decoder.Decoder, error) {
env := createBuildEnvVars(b.info, dirs) // Создаём переменные окружения для сборки
fakeroot := handlers.FakerootExecHandler(2 * time.Second) // Настраиваем "fakeroot" для выполнения
runner, err := interp.New(
interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение
interp.StdIO(os.Stdin, os.Stdout, os.Stderr), // Устанавливаем стандартный ввод-вывод
interp.ExecHandler(helpers.Helpers.ExecHandler(fakeroot)), // Обрабатываем выполнение через fakeroot
)
if err != nil {
return nil, err
}
err = runner.Run(ctx, fl) // Запускаем скрипт
if err != nil {
return nil, err
}
return decoder.New(b.info, runner), nil // Возвращаем новый декодер
}
// Функция performChecks проверяет различные аспекты в системе, чтобы убедиться, что пакет может быть установлен.
func (b *Builder) performChecks(ctx context.Context, vars *types.BuildVars, installed map[string]string) (bool, error) {
if !cpu.IsCompatibleWith(cpu.Arch(), vars.Architectures) { // Проверяем совместимость архитектуры
cont, err := cliutils.YesNoPrompt(
ctx,
gotext.Get("Your system's CPU architecture doesn't match this package. Do you want to build anyway?"),
b.opts.Interactive,
true,
)
if err != nil {
return false, err
}
if !cont {
return false, nil
}
}
if instVer, ok := installed[vars.Name]; ok { // Если пакет уже установлен, выводим предупреждение
slog.Warn(gotext.Get("This package is already installed"),
"name", vars.Name,
"version", instVer,
)
}
return true, nil
}
// Функция installBuildDeps устанавливает все зависимости сборки, которые еще не установлены, и возвращает
// срез, содержащий имена всех установленных пакетов.
func (b *Builder) installBuildDeps(ctx context.Context, vars *types.BuildVars) ([]string, error) {
var buildDeps []string
if len(vars.BuildDepends) > 0 {
deps, err := removeAlreadyInstalled(b.opts, vars.BuildDepends)
if err != nil {
return nil, err
}
found, notFound, err := repos.FindPkgs(ctx, deps) // Находим пакеты-зависимости
if err != nil {
return nil, err
}
slog.Info(gotext.Get("Installing build dependencies")) // Логгируем установку зависимостей
flattened := cliutils.FlattenPkgs(ctx, found, "install", b.opts.Interactive) // Уплощаем список зависимостей
buildDeps = packageNames(flattened)
InstallPkgs(ctx, flattened, notFound, b.opts) // Устанавливаем пакеты
}
return buildDeps, nil
}
func (b *Builder) buildALRDeps(ctx context.Context, vars *types.BuildVars) (builtPaths, builtNames, repoDeps []string, err error) {
if len(vars.Depends) > 0 {
slog.Info(gotext.Get("Installing dependencies"))
found, notFound, err := repos.FindPkgs(ctx, vars.Depends) // Поиск зависимостей
if err != nil {
return nil, nil, nil, err
}
repoDeps = notFound
// Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез
pkgs := cliutils.FlattenPkgs(ctx, found, "install", b.opts.Interactive)
for _, pkg := range pkgs {
newOpts := b.opts
UpdateOpts(ctx, &newOpts, &pkg)
newB := New(
ctx,
newOpts,
b.repos,
b.info,
b.config,
)
// Собираем зависимости
pkgPaths, pkgNames, err := newB.BuildPackage(ctx)
if err != nil {
return nil, nil, nil, err
}
// Добавляем пути всех собранных пакетов в builtPaths
builtPaths = append(builtPaths, pkgPaths...)
// Добавляем пути всех собранных пакетов в builtPaths
builtNames = append(builtNames, pkgNames...)
// Добавляем имя текущего пакета в builtNames
builtNames = append(builtNames, filepath.Base(filepath.Dir(newOpts.Script)))
}
}
// Удаляем возможные дубликаты, которые могут быть введены, если
// несколько зависимостей зависят от одних и тех же пакетов.
repoDeps = removeDuplicates(repoDeps)
builtPaths = removeDuplicates(builtPaths)
builtNames = removeDuplicates(builtNames)
return builtPaths, builtNames, repoDeps, nil
}
func (b *Builder) getSources(ctx context.Context, dirs types.Directories, bv *types.BuildVars) error {
if len(bv.Sources) != len(bv.Checksums) {
slog.Error(gotext.Get("The checksums array must be the same length as sources"))
os.Exit(1)
}
for i, src := range bv.Sources {
opts := dl.Options{
Name: fmt.Sprintf("%s[%d]", bv.Name, i),
URL: src,
Destination: dirs.SrcDir,
Progress: os.Stderr,
LocalDir: dirs.ScriptDir,
}
if !strings.EqualFold(bv.Checksums[i], "SKIP") {
// Если контрольная сумма содержит двоеточие, используйте часть до двоеточия
// как алгоритм, а часть после как фактическую контрольную сумму.
// В противном случае используйте sha256 по умолчанию с целой строкой как контрольной суммой.
algo, hashData, ok := strings.Cut(bv.Checksums[i], ":")
if ok {
checksum, err := hex.DecodeString(hashData)
if err != nil {
return err
}
opts.Hash = checksum
opts.HashAlgorithm = algo
} else {
checksum, err := hex.DecodeString(bv.Checksums[i])
if err != nil {
return err
}
opts.Hash = checksum
}
}
opts.DlCache = dlcache.New(b.config)
err := dl.Download(ctx, opts)
if err != nil {
return err
}
}
return nil
}
// Функция removeBuildDeps спрашивает у пользователя, хочет ли он удалить зависимости,
// установленные для сборки. Если да, использует менеджер пакетов для их удаления.
func (b *Builder) removeBuildDeps(ctx context.Context, buildDeps []string) error {
if len(buildDeps) > 0 {
remove, err := cliutils.YesNoPrompt(
ctx,
gotext.Get("Would you like to remove the build dependencies?"),
b.opts.Interactive,
false,
)
if err != nil {
return err
}
if remove {
err = b.opts.Manager.Remove(
&manager.Opts{
AsRoot: true,
NoConfirm: true,
},
buildDeps...,
)
if err != nil {
return err
}
}
}
return nil
}
type FunctionsOutput struct {
Contents *[]string
}
// Функция executeFunctions выполняет специальные функции ALR, такие как version(), prepare() и т.д.
func (b *Builder) executeFunctions(
ctx context.Context,
dec *decoder.Decoder,
dirs types.Directories,
vars *types.BuildVars,
) (*FunctionsOutput, error) {
version, ok := dec.GetFunc("version")
if ok {
slog.Info(gotext.Get("Executing version()"))
buf := &bytes.Buffer{}
err := version(
ctx,
interp.Dir(dirs.SrcDir),
interp.StdIO(os.Stdin, buf, os.Stderr),
)
if err != nil {
return nil, err
}
newVer := strings.TrimSpace(buf.String())
err = setVersion(ctx, dec.Runner, newVer)
if err != nil {
return nil, err
}
vars.Version = newVer
slog.Info(gotext.Get("Updating version"), "new", newVer)
}
prepare, ok := dec.GetFunc("prepare")
if ok {
slog.Info(gotext.Get("Executing prepare()"))
err := prepare(ctx, interp.Dir(dirs.SrcDir))
if err != nil {
return nil, 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 nil, err
}
}
var packageFuncName string
if b.opts.Package == "" {
packageFuncName = "package"
} else {
packageFuncName = fmt.Sprintf("package_%s", b.opts.Package)
}
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
}
}
output := &FunctionsOutput{}
var filesFuncName string
if b.opts.Package == "" {
filesFuncName = "files"
} else {
filesFuncName = fmt.Sprintf("files_%s", b.opts.Package)
}
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
}