// 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 . package build import ( "bytes" "context" "errors" "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/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" finddeps "gitea.plemya-x.ru/Plemya-x/ALR/pkg/build/find_deps" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" ) type LocalScriptExecutor struct { cfg Config } func NewLocalScriptExecutor(cfg Config) *LocalScriptExecutor { return &LocalScriptExecutor{ cfg, } } func (e *LocalScriptExecutor) ReadScript(ctx context.Context, scriptPath string) (*ScriptFile, error) { fl, err := readScript(scriptPath) if err != nil { return nil, err } return &ScriptFile{ Path: scriptPath, File: fl, }, nil } func (e *LocalScriptExecutor) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *ScriptFile) (string, []*types.BuildVars, error) { varsOfPackages := []*types.BuildVars{} scriptDir := filepath.Dir(sf.Path) env := createBuildEnvVars(input.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.ReadDirHandler2(handlers.RestrictedReadDir(scriptDir)), // Ограничиваем чтение директорий interp.StatHandler(handlers.RestrictedStat(scriptDir)), // Ограничиваем доступ к статистике файлов interp.OpenHandler(handlers.RestrictedOpen(scriptDir)), // Ограничиваем открытие файлов interp.Dir(scriptDir), ) if err != nil { return "", nil, err } err = runner.Run(ctx, sf.File) // Запускаем скрипт if err != nil { return "", nil, err } dec := decoder.New(input.info, runner) // Создаём новый декодер type packages struct { BasePkgName string `sh:"basepkg_name"` Names []string `sh:"name"` } var pkgs packages err = dec.DecodeVars(&pkgs) if err != nil { return "", nil, err } if len(pkgs.Names) == 0 { return "", nil, errors.New("package name is missing") } var vars types.BuildVars if len(pkgs.Names) == 1 { err = dec.DecodeVars(&vars) // Декодируем переменные if err != nil { return "", nil, err } varsOfPackages = append(varsOfPackages, &vars) return vars.Name, varsOfPackages, nil } if len(input.packages) == 0 { return "", nil, errors.New("script has multiple packages but package is not specified") } for _, pkgName := range input.packages { var preVars types.BuildVarsPre funcName := fmt.Sprintf("meta_%s", pkgName) meta, ok := dec.GetFuncWithSubshell(funcName) if !ok { return "", nil, errors.New("func is missing") } r, err := meta(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 = pkgName vars.Base = pkgs.BasePkgName varsOfPackages = append(varsOfPackages, &vars) } return pkgs.BasePkgName, varsOfPackages, nil } type SecondPassResult struct { BuiltPaths []string BuiltNames []string } 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 *ScriptFile, varsOfPackages []*types.BuildVars, repoDeps []string, builtNames []string, basePkg string, ) (*SecondPassResult, 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.Stdout, 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.Base != "" { packageName = vars.Name } pkgFormat := input.pkgFormat funcOut, err := e.ExecutePackageFunctions( ctx, dec, dirs, packageName, ) if err != nil { return nil, err } slog.Info(gotext.Get("Building package metadata"), "name", basePkg) pkgInfo, err := buildPkgMetadata( ctx, input, vars, dirs, append( repoDeps, builtNames..., ), 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(dirs.BaseDir, pkgName) // Определяем путь к пакету pkgFile, err := os.Create(pkgPath) if err != nil { return nil, err } err = packager.Package(pkgInfo, pkgFile) if err != nil { return nil, err } builtPaths = append(builtPaths, pkgPath) builtNames = append(builtNames, vars.Name) } return &SecondPassResult{ BuiltPaths: builtPaths, BuiltNames: builtNames, }, nil } func buildPkgMetadata( ctx context.Context, input interface { OsInfoProvider BuildOptsProvider PkgFormatProvider RepositoryProvider }, vars *types.BuildVars, dirs types.Directories, deps []string, preferedContents *[]string, ) (*nfpm.Info, error) { pkgInfo := getBasePkgInfo(vars, input) pkgInfo.Description = vars.Description pkgInfo.Platform = "linux" pkgInfo.Homepage = vars.Homepage pkgInfo.License = strings.Join(vars.Licenses, ", ") pkgInfo.Maintainer = vars.Maintainer pkgInfo.Overridables = nfpm.Overridables{ Conflicts: append(vars.Conflicts, vars.Name), Replaces: vars.Replaces, Provides: append(vars.Provides, vars.Name), Depends: deps, } pkgFormat := input.PkgFormat() info := input.OSRelease() if pkgFormat == "apk" { // Alpine отказывается устанавливать пакеты, которые предоставляют сами себя, поэтому удаляем такие элементы pkgInfo.Overridables.Provides = slices.DeleteFunc(pkgInfo.Overridables.Provides, func(s string) bool { return s == pkgInfo.Name }) } 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 } pkgInfo.Overridables.Contents = contents if len(vars.AutoProv) == 1 && decoder.IsTruthy(vars.AutoProv[0]) { f := finddeps.New(info, pkgFormat) err = f.FindProvides(ctx, pkgInfo, dirs, vars.AutoProvSkipList) if err != nil { return nil, err } } if len(vars.AutoReq) == 1 && decoder.IsTruthy(vars.AutoReq[0]) { f := finddeps.New(info, pkgFormat) err = f.FindRequires(ctx, pkgInfo, dirs, vars.AutoReqSkipList) 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 }