This commit is contained in:
2025-04-12 15:51:39 +03:00
parent 5ca34a572a
commit 8070112bf2
45 changed files with 2728 additions and 1196 deletions

File diff suppressed because it is too large Load Diff

View File

@ -277,7 +277,7 @@ meta_bar() {
fl, err := syntax.NewParser().Parse(strings.NewReader(tc.Script), "alr.sh")
assert.NoError(t, err)
_, allVars, err := b.executeFirstPass(fl)
_, allVars, err := b.scriptExecutor.ExecuteSecondPass(fl)
assert.NoError(t, err)
tc.Expected(t, allVars)

69
pkg/build/cache.go Normal file
View File

@ -0,0 +1,69 @@
// 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 (
"context"
"os"
"path/filepath"
"github.com/goreleaser/nfpm/v2"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
)
type Cache struct {
cfg Config
}
func (c *Cache) CheckForBuiltPackage(
ctx context.Context,
input *BuildInput,
vars *types.BuildVars,
) (string, bool, error) {
filename, err := pkgFileName(input, vars)
if err != nil {
return "", false, err
}
pkgPath := filepath.Join(getBaseDir(c.cfg, vars.Name), filename)
_, err = os.Stat(pkgPath)
if err != nil {
return "", false, nil
}
return pkgPath, true, nil
}
func pkgFileName(
input interface {
OsInfoProvider
PkgFormatProvider
RepositoryProvider
},
vars *types.BuildVars,
) (string, error) {
pkgInfo := getBasePkgInfo(vars, input)
packager, err := nfpm.Get(input.PkgFormat())
if err != nil {
return "", err
}
return packager.ConventionalFileName(pkgInfo), nil
}

74
pkg/build/checker.go Normal file
View File

@ -0,0 +1,74 @@
// 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 (
"context"
"log/slog"
"github.com/leonelquinteros/gotext"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
)
type Checker struct {
mgr manager.Manager
}
func (c *Checker) PerformChecks(
ctx context.Context,
input *BuildInput,
vars *types.BuildVars,
) (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?"),
input.opts.Interactive,
true,
)
if err != nil {
return false, err
}
if !cont {
return false, nil
}
}
installed, err := c.mgr.ListInstalled(nil)
if err != nil {
return false, err
}
filename, err := pkgFileName(input, vars)
if err != nil {
return false, err
}
if instVer, ok := installed[filename]; ok { // Если пакет уже установлен, выводим предупреждение
slog.Warn(gotext.Get("This package is already installed"),
"name", vars.Name,
"version", instVer,
)
}
return true, nil
}

71
pkg/build/dirs.go Normal file
View File

@ -0,0 +1,71 @@
// 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 (
"path/filepath"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
)
type BaseDirProvider interface {
BaseDir() string
}
type SrcDirProvider interface {
SrcDir() string
}
type PkgDirProvider interface {
PkgDir() string
}
type ScriptDirProvider interface {
ScriptDir() string
}
func getDirs(
cfg Config,
scriptPath string,
basePkg string,
) (types.Directories, error) {
pkgsDir := cfg.GetPaths().PkgsDir
scriptPath, err := filepath.Abs(scriptPath)
if err != nil {
return types.Directories{}, err
}
baseDir := filepath.Join(pkgsDir, basePkg)
return types.Directories{
BaseDir: getBaseDir(cfg, basePkg),
SrcDir: getSrcDir(cfg, basePkg),
PkgDir: filepath.Join(baseDir, "pkg"),
ScriptDir: getScriptDir(scriptPath),
}, nil
}
func getBaseDir(cfg Config, basePkg string) string {
return filepath.Join(cfg.GetPaths().PkgsDir, basePkg)
}
func getSrcDir(cfg Config, basePkg string) string {
return filepath.Join(getBaseDir(cfg, basePkg), "src")
}
func getScriptDir(scriptPath string) string {
return filepath.Dir(scriptPath)
}

61
pkg/build/installer.go Normal file
View File

@ -0,0 +1,61 @@
// 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 (
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
)
func NewInstaller(
repos PackageFinder,
mgr manager.Manager,
) *Installer {
return &Installer{
repos: repos,
mgr: mgr,
}
}
type Installer struct {
repos PackageFinder
mgr manager.Manager
}
func (i *Installer) InstallLocal(paths []string) error {
return i.mgr.InstallLocal(nil, paths...)
}
func (i *Installer) Install(pkgs []string) error {
return i.mgr.Install(nil, pkgs...)
}
func (i *Installer) RemoveAlreadyInstalled(pkgs []string) ([]string, error) {
filteredPackages := []string{}
for _, dep := range pkgs {
installed, err := i.mgr.IsInstalled(dep)
if err != nil {
return nil, err
}
if installed {
continue
}
filteredPackages = append(filteredPackages, dep)
}
return filteredPackages, nil
}

68
pkg/build/main_build.go Normal file
View File

@ -0,0 +1,68 @@
// 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 (
"log/slog"
"os"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
)
func NewMainBuilder(
cfg Config,
repos PackageFinder,
) *Builder {
slog.Info("", "uid", os.Geteuid(), "gid", os.Getegid())
s, err := GetSafeScriptExecutor()
if err != nil {
slog.Info("i will panic")
panic(err)
}
mgr := manager.Detect()
installerExecutor, err := GetSafeInstaller()
if err != nil {
slog.Info("i will panic")
panic(err)
}
builder := &Builder{
scriptExecutor: s,
cacheExecutor: &Cache{
cfg,
},
scriptResolver: &ScriptResolver{
cfg,
},
scriptViewerExecutor: &ScriptViewer{
config: cfg,
},
checkerExecutor: &Checker{
mgr,
},
installerExecutor: installerExecutor,
sourceExecutor: &SourceDownloader{
cfg,
},
repos: repos,
}
return builder
}

263
pkg/build/safe.go Normal file
View File

@ -0,0 +1,263 @@
// 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 (
"context"
"log/slog"
"net/rpc"
"os"
"os/exec"
"syscall"
"github.com/hashicorp/go-plugin"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
)
var HandshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "ALR_PLUGIN",
MagicCookieValue: "-",
}
type ScriptExecutorPlugin struct {
Impl ScriptExecutor
}
type ScriptExecutorRPCServer struct {
Impl ScriptExecutor
}
// =============================
//
// ReadScript
//
func (s *ScriptExecutorRPC) ReadScript(ctx context.Context, scriptPath string) (*ScriptFile, error) {
var resp *ScriptFile
err := s.client.Call("Plugin.ReadScript", scriptPath, &resp)
return resp, err
}
func (s *ScriptExecutorRPCServer) ReadScript(scriptPath string, resp *ScriptFile) error {
file, err := s.Impl.ReadScript(context.Background(), scriptPath)
if err != nil {
return err
}
*resp = *file
return nil
}
// =============================
//
// ExecuteFirstPass
//
type ExecuteFirstPassArgs struct {
Input *BuildInput
Sf *ScriptFile
}
type ExecuteFirstPassResp struct {
BasePkg string
VarsOfPackages []*types.BuildVars
}
func (s *ScriptExecutorRPC) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *ScriptFile) (string, []*types.BuildVars, error) {
var resp *ExecuteFirstPassResp
err := s.client.Call("Plugin.ExecuteFirstPass", &ExecuteFirstPassArgs{
Input: input,
Sf: sf,
}, &resp)
if err != nil {
return "", nil, err
}
return resp.BasePkg, resp.VarsOfPackages, nil
}
func (s *ScriptExecutorRPCServer) ExecuteFirstPass(args *ExecuteFirstPassArgs, resp *ExecuteFirstPassResp) error {
basePkg, varsOfPackages, err := s.Impl.ExecuteFirstPass(context.Background(), args.Input, args.Sf)
if err != nil {
return err
}
*resp = ExecuteFirstPassResp{
BasePkg: basePkg,
VarsOfPackages: varsOfPackages,
}
return nil
}
// =============================
//
// PrepareDirs
//
type PrepareDirsArgs struct {
Input *BuildInput
BasePkg string
}
func (s *ScriptExecutorRPC) PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error {
err := s.client.Call("Plugin.PrepareDirs", &PrepareDirsArgs{
Input: input,
BasePkg: basePkg,
}, nil)
if err != nil {
return err
}
return err
}
func (s *ScriptExecutorRPCServer) PrepareDirs(args *PrepareDirsArgs, reply *struct{}) error {
err := s.Impl.PrepareDirs(
context.Background(),
args.Input,
args.BasePkg,
)
if err != nil {
return err
}
return err
}
// =============================
//
// ExecuteSecondPass
//
type ExecuteSecondPassArgs struct {
Input *BuildInput
Sf *ScriptFile
VarsOfPackages []*types.BuildVars
RepoDeps []string
BuiltNames []string
BasePkg string
}
func (s *ScriptExecutorRPC) ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *ScriptFile,
varsOfPackages []*types.BuildVars,
repoDeps []string,
builtNames []string,
basePkg string,
) (*SecondPassResult, error) {
var resp *SecondPassResult
err := s.client.Call("Plugin.ExecuteSecondPass", &ExecuteSecondPassArgs{
Input: input,
Sf: sf,
VarsOfPackages: varsOfPackages,
RepoDeps: repoDeps,
BuiltNames: builtNames,
BasePkg: basePkg,
}, &resp)
if err != nil {
return nil, err
}
return resp, nil
}
func (s *ScriptExecutorRPCServer) ExecuteSecondPass(args *ExecuteSecondPassArgs, resp *SecondPassResult) error {
res, err := s.Impl.ExecuteSecondPass(
context.Background(),
args.Input,
args.Sf,
args.VarsOfPackages,
args.RepoDeps,
args.BuiltNames,
args.BasePkg,
)
if err != nil {
return err
}
*resp = *res
return err
}
//
// ============================
//
func (p *ScriptExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &ScriptExecutorRPCServer{Impl: p.Impl}, nil
}
func (p *ScriptExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &ScriptExecutorRPC{client: c}, nil
}
type ScriptExecutorRPC struct {
client *rpc.Client
}
var pluginMap = map[string]plugin.Plugin{
"script-executor": &ScriptExecutorPlugin{},
"installer": &InstallerPlugin{},
}
func GetSafeScriptExecutor() (ScriptExecutor, error) {
executable, err := os.Executable()
if err != nil {
return nil, err
}
cmd := exec.Command(executable, "_internal-safe-script-executor")
cmd.Env = []string{
"HOME=/var/cache/alr",
"LOGNAME=alr",
"USER=alr",
"PATH=/usr/bin:/bin:/usr/local/bin",
}
uid, gid, err := utils.GetUidGidAlrUser()
if err != nil {
return nil, err
}
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
},
}
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: HandshakeConfig,
Plugins: pluginMap,
Cmd: cmd,
Logger: logger.GetHCLoggerAdapter(),
SkipHostEnv: true,
})
rpcClient, err := client.Client()
if err != nil {
slog.Info("1")
return nil, err
}
raw1, err := rpcClient.Dispense("script-executor")
if err != nil {
return nil, err
}
return raw1.(ScriptExecutor), nil
}

132
pkg/build/safe_installer.go Normal file
View File

@ -0,0 +1,132 @@
// 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 (
"log/slog"
"net/rpc"
"os"
"os/exec"
"syscall"
"github.com/hashicorp/go-plugin"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
)
type InstallerPlugin struct {
Impl InstallerExecutor
}
type InstallerRPC struct {
client *rpc.Client
}
type InstallerRPCServer struct {
Impl InstallerExecutor
}
func (r *InstallerRPC) InstallLocal(paths []string) error {
return r.client.Call("Plugin.InstallLocal", paths, nil)
}
func (s *InstallerRPCServer) InstallLocal(paths []string, reply *struct{}) error {
slog.Warn("install", "paths", paths)
return s.Impl.InstallLocal(paths)
}
func (r *InstallerRPC) Install(pkgs []string) error {
return r.client.Call("Plugin.Install", pkgs, nil)
}
func (s *InstallerRPCServer) Install(pkgs []string, reply *struct{}) error {
slog.Debug("install", "pkgs", pkgs)
return s.Impl.Install(pkgs)
}
func (r *InstallerRPC) RemoveAlreadyInstalled(paths []string) ([]string, error) {
err := r.client.Call("Plugin.RemoveAlreadyInstalled", paths, nil)
return nil, err
}
func (s *InstallerRPCServer) RemoveAlreadyInstalled(pkgs []string, res *[]string) error {
vars, err := s.Impl.RemoveAlreadyInstalled(pkgs)
if err != nil {
return err
}
*res = vars
return nil
}
func (p *InstallerPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &InstallerRPC{client: c}, nil
}
func (p *InstallerPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &InstallerRPCServer{Impl: p.Impl}, nil
}
func GetSafeInstaller() (InstallerExecutor, error) {
executable, err := os.Executable()
if err != nil {
return nil, err
}
cmd := exec.Command(executable, "_internal-installer")
cmd.Env = append(os.Environ(),
"HOME=/var/cache/alr",
"LOGNAME=alr",
"USER=alr",
"PATH=/usr/bin:/bin:/usr/local/bin",
"ALR_LOG_LEVEL=DEBUG",
"XDG_SESSION_CLASS=user",
)
uid, gid, err := utils.GetUidGidAlrUser()
if err != nil {
return nil, err
}
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uint32(uid),
Gid: uint32(gid),
},
}
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: HandshakeConfig,
Plugins: pluginMap,
Cmd: cmd,
Logger: logger.GetHCLoggerAdapter(),
SkipHostEnv: true,
UnixSocketConfig: &plugin.UnixSocketConfig{
Group: "alr",
},
SyncStderr: os.Stderr,
})
rpcClient, err := client.Client()
if err != nil {
slog.Info("1")
return nil, err
}
raw1, err := rpcClient.Dispense("installer")
if err != nil {
return nil, err
}
return raw1.(InstallerExecutor), nil
}

View File

@ -0,0 +1,434 @@
// 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"
"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)), // Ограничиваем открытие файлов
)
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
}

View File

@ -0,0 +1,53 @@
// 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 (
"context"
"path/filepath"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
)
type ScriptResolver struct {
cfg Config
}
type ScriptInfo struct {
Script string
Repository string
}
func (s *ScriptResolver) ResolveScript(
ctx context.Context,
pkg *db.Package,
) *ScriptInfo {
var repository, script string
repodir := s.cfg.GetPaths().RepoDir
repository = pkg.Repository
if pkg.BasePkgName != "" {
script = filepath.Join(repodir, repository, pkg.BasePkgName, "alr.sh")
} else {
script = filepath.Join(repodir, repository, pkg.Name, "alr.sh")
}
return &ScriptInfo{
Repository: repository,
Script: script,
}
}

46
pkg/build/script_view.go Normal file
View File

@ -0,0 +1,46 @@
// 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 (
"context"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
)
type ScriptViewerConfig interface {
PagerStyle() string
}
type ScriptViewer struct {
config ScriptViewerConfig
}
func (s *ScriptViewer) ViewScript(
ctx context.Context,
input *BuildInput,
sf *ScriptFile,
basePkg string,
) error {
return cliutils.PromptViewScript(
ctx,
sf.Path,
basePkg,
s.config.PagerStyle(),
input.opts.Interactive,
)
}

View File

@ -0,0 +1,86 @@
// 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 (
"context"
"encoding/hex"
"fmt"
"os"
"strings"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dl"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dlcache"
)
type SourceDownloader struct {
cfg Config
}
func NewSourceDownloader(cfg Config) *SourceDownloader {
return &SourceDownloader{
cfg,
}
}
func (s *SourceDownloader) DownloadSources(
ctx context.Context,
input *BuildInput,
basePkg string,
si SourcesInput,
) error {
for i, src := range si.Sources {
opts := dl.Options{
Name: fmt.Sprintf("[%d]", i),
URL: src,
Destination: getSrcDir(s.cfg, basePkg),
Progress: os.Stderr,
LocalDir: getScriptDir(input.script),
}
if !strings.EqualFold(si.Checksums[i], "SKIP") {
// Если контрольная сумма содержит двоеточие, используйте часть до двоеточия
// как алгоритм, а часть после как фактическую контрольную сумму.
// В противном случае используйте sha256 по умолчанию с целой строкой как контрольной суммой.
algo, hashData, ok := strings.Cut(si.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(si.Checksums[i])
if err != nil {
return err
}
opts.Hash = checksum
}
}
opts.DlCache = dlcache.New(s.cfg)
err := dl.Download(ctx, opts)
if err != nil {
return err
}
}
return nil
}

View File

@ -39,7 +39,6 @@ import (
"github.com/goreleaser/nfpm/v2/files"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
@ -173,19 +172,23 @@ func buildContents(vars *types.BuildVars, dirs types.Directories, preferedConten
var RegexpALRPackageName = regexp.MustCompile(`^(?P<package>[^+]+)\+alr-(?P<repo>.+)$`)
func getBasePkgInfo(vars *types.BuildVars, info *distro.OSRelease, opts *types.BuildOpts) *nfpm.Info {
func getBasePkgInfo(vars *types.BuildVars, input interface {
RepositoryProvider
OsInfoProvider
},
) *nfpm.Info {
return &nfpm.Info{
Name: fmt.Sprintf("%s+alr-%s", vars.Name, opts.Repository),
Name: fmt.Sprintf("%s+alr-%s", vars.Name, input.Repository()),
Arch: cpu.Arch(),
Version: vars.Version,
Release: overrides.ReleasePlatformSpecific(vars.Release, info),
Release: overrides.ReleasePlatformSpecific(vars.Release, input.OSRelease()),
Epoch: strconv.FormatUint(uint64(vars.Epoch), 10),
}
}
// Функция getPkgFormat возвращает формат пакета из менеджера пакетов,
// или ALR_PKG_FORMAT, если он установлен.
func getPkgFormat(mgr manager.Manager) string {
func GetPkgFormat(mgr manager.Manager) string {
pkgFormat := mgr.Format()
if format, ok := os.LookupEnv("ALR_PKG_FORMAT"); ok {
pkgFormat = format
@ -272,25 +275,9 @@ func setVersion(ctx context.Context, r *interp.Runner, to string) error {
return r.Run(ctx, fl)
}
*/
// Returns not installed dependencies
func removeAlreadyInstalled(opts types.BuildOpts, dependencies []string) ([]string, error) {
filteredPackages := []string{}
for _, dep := range dependencies {
installed, err := opts.Manager.IsInstalled(dep)
if err != nil {
return nil, err
}
if installed {
continue
}
filteredPackages = append(filteredPackages, dep)
}
return filteredPackages, nil
}
// Функция packageNames возвращает имена всех предоставленных пакетов.
/*
func packageNames(pkgs []db.Package) []string {
names := make([]string, len(pkgs))
for i, p := range pkgs {
@ -298,6 +285,7 @@ func packageNames(pkgs []db.Package) []string {
}
return names
}
*/
// Функция removeDuplicates убирает любые дубликаты из предоставленного среза.
func removeDuplicates(slice []string) []string {

View File

@ -66,6 +66,7 @@ func (a *APTRpm) Install(opts *Opts, pkgs ...string) error {
cmd := a.getCmd(opts, "apt-get", "install")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
cmd.Stdout = cmd.Stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("apt-get: install: %w", err)

View File

@ -268,6 +268,7 @@ func (rs *Repos) processRepoChangesRunner(repoDir, scriptDir string) (*interp.Ru
interp.StatHandler(handlers.RestrictedStat(repoDir)),
interp.OpenHandler(handlers.RestrictedOpen(repoDir)),
interp.StdIO(handlers.NopRWC{}, handlers.NopRWC{}, handlers.NopRWC{}),
interp.Dir(scriptDir),
)
}