diff --git a/Makefile b/Makefile
index ac59441..c1bed44 100644
--- a/Makefile
+++ b/Makefile
@@ -37,6 +37,7 @@ install: \
$(INSTALED_BIN): $(BIN)
install -Dm755 $< $@
+ setcap cap_setuid,cap_setgid+ep $(INSTALED_BIN)
$(INSTALLED_BASH_COMPLETION): $(BASH_COMPLETION)
install -Dm755 $< $@
diff --git a/assets/coverage-badge.svg b/assets/coverage-badge.svg
index ac4f67b..d9be4c1 100644
--- a/assets/coverage-badge.svg
+++ b/assets/coverage-badge.svg
@@ -11,7 +11,7 @@
coverage
coverage
- 19.0%
- 19.0%
+ 16.3%
+ 16.3%
diff --git a/build.go b/build.go
index 668d461..e4a7032 100644
--- a/build.go
+++ b/build.go
@@ -28,14 +28,12 @@ import (
"github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2"
- "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
- database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
+ appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/osutils"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/build"
- "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"
)
func BuildCmd() *cli.Command {
@@ -66,32 +64,67 @@ func BuildCmd() *cli.Command {
},
},
Action: func(c *cli.Context) error {
- ctx := c.Context
- cfg := config.New()
- err := cfg.Load()
- if err != nil {
- slog.Error(gotext.Get("Error loading config"), "err", err)
- os.Exit(1)
+ if err := utils.EnuseIsPrivilegedGroupMember(); err != nil {
+ return err
}
- db := database.New(cfg)
- rs := repos.New(cfg, db)
- err = db.Init(ctx)
+ wd, err := os.Getwd()
if err != nil {
- slog.Error(gotext.Get("Error initialization database"), "err", err)
- os.Exit(1)
+ return cliutils.FormatCliExit(gotext.Get("Error getting working directory"), err)
}
+ wd, wdCleanup, err := Mount(wd)
+ if err != nil {
+ return err
+ }
+ defer wdCleanup()
+
+ ctx := c.Context
+
+ deps, err := appbuilder.
+ New(ctx).
+ WithConfig().
+ WithDB().
+ WithReposNoPull().
+ WithDistroInfo().
+ WithManager().
+ Build()
+ if err != nil {
+ return cli.Exit(err, 1)
+ }
+ defer deps.Defer()
+
var script string
var packages []string
- repository := "default"
- repoDir := cfg.GetPaths().RepoDir
+ var res *build.BuildResult
+
+ var scriptArgs *build.BuildPackageFromScriptArgs
+ var dbArgs *build.BuildPackageFromDbArgs
+
+ buildArgs := &build.BuildArgs{
+ Opts: &types.BuildOpts{
+ Clean: c.Bool("clean"),
+ Interactive: c.Bool("interactive"),
+ },
+ PkgFormat_: build.GetPkgFormat(deps.Manager),
+ Info: deps.Info,
+ }
switch {
case c.IsSet("script"):
- script = c.String("script")
+ script, err = filepath.Abs(c.String("script"))
+ if err != nil {
+ return cliutils.FormatCliExit(gotext.Get("Cannot get absolute script path"), err)
+ }
+
packages = append(packages, c.String("script-package"))
+
+ scriptArgs = &build.BuildPackageFromScriptArgs{
+ Script: script,
+ Packages: packages,
+ BuildArgs: *buildArgs,
+ }
case c.IsSet("package"):
// TODO: handle multiple packages
packageInput := c.String("package")
@@ -104,86 +137,97 @@ func BuildCmd() *cli.Command {
packageSearch = arr[0]
}
- pkgs, _, _ := rs.FindPkgs(ctx, []string{packageSearch})
- pkg, ok := pkgs[packageSearch]
- if len(pkg) < 1 || !ok {
- slog.Error(gotext.Get("Package not found"))
- os.Exit(1)
+ pkgs, _, err := deps.Repos.FindPkgs(ctx, []string{packageSearch})
+ if err != nil {
+ return cliutils.FormatCliExit("failed to find pkgs", err)
}
- repository = pkg[0].Repository
+ pkg := cliutils.FlattenPkgs(ctx, pkgs, "build", c.Bool("interactive"))
+
+ if len(pkg) < 1 {
+ return cliutils.FormatCliExit(gotext.Get("Package not found"), nil)
+ }
if pkg[0].BasePkgName != "" {
- script = filepath.Join(repoDir, repository, pkg[0].BasePkgName, "alr.sh")
packages = append(packages, pkg[0].Name)
- } else {
- script = filepath.Join(repoDir, repository, pkg[0].Name, "alr.sh")
+ }
+
+ dbArgs = &build.BuildPackageFromDbArgs{
+ Package: &pkg[0],
+ Packages: packages,
+ BuildArgs: *buildArgs,
}
default:
- script = filepath.Join(repoDir, "alr.sh")
+ return cliutils.FormatCliExit(gotext.Get("Nothing to build"), nil)
}
- // Проверка автоматического пулла репозиториев
- if cfg.AutoPull() {
- err := rs.Pull(ctx, cfg.Repos())
+ if scriptArgs != nil {
+ scriptFile := filepath.Base(scriptArgs.Script)
+ newScriptDir, scriptDirCleanup, err := Mount(filepath.Dir(scriptArgs.Script))
if err != nil {
- slog.Error(gotext.Get("Error pulling repositories"), "err", err)
- os.Exit(1)
+ return err
}
+ defer scriptDirCleanup()
+ scriptArgs.Script = filepath.Join(newScriptDir, scriptFile)
}
- // Обнаружение менеджера пакетов
- mgr := manager.Detect()
- if mgr == nil {
- slog.Error(gotext.Get("Unable to detect a supported package manager on the system"))
- os.Exit(1)
+ if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
+ return err
}
- info, err := distro.ParseOSRelease(ctx)
+ installer, installerClose, err := build.GetSafeInstaller()
if err != nil {
- slog.Error(gotext.Get("Error parsing os release"), "err", err)
- os.Exit(1)
+ return err
+ }
+ defer installerClose()
+
+ if err := utils.ExitIfCantSetNoNewPrivs(); err != nil {
+ return err
}
- builder := build.NewBuilder(
- ctx,
- types.BuildOpts{
- Packages: packages,
- Repository: repository,
- Script: script,
- Manager: mgr,
- Clean: c.Bool("clean"),
- Interactive: c.Bool("interactive"),
- },
- rs,
- info,
- cfg,
+ scripter, scripterClose, err := build.GetSafeScriptExecutor()
+ if err != nil {
+ return err
+ }
+ defer scripterClose()
+
+ builder, err := build.NewMainBuilder(
+ deps.Cfg,
+ deps.Manager,
+ deps.Repos,
+ scripter,
+ installer,
)
-
- // Сборка пакета
- pkgPaths, _, err := builder.BuildPackage(ctx)
if err != nil {
- slog.Error(gotext.Get("Error building package"), "err", err)
- os.Exit(1)
+ return err
}
- // Получение текущей рабочей директории
- wd, err := os.Getwd()
- if err != nil {
- slog.Error(gotext.Get("Error getting working directory"), "err", err)
- os.Exit(1)
+ if scriptArgs != nil {
+ res, err = builder.BuildPackageFromScript(
+ ctx,
+ scriptArgs,
+ )
+ } else if dbArgs != nil {
+ res, err = builder.BuildPackageFromDb(
+ ctx,
+ dbArgs,
+ )
}
- // Перемещение собранных пакетов в рабочую директорию
- for _, pkgPath := range pkgPaths {
+ if err != nil {
+ return cliutils.FormatCliExit(gotext.Get("Error building package"), err)
+ }
+
+ for _, pkgPath := range res.PackagePaths {
name := filepath.Base(pkgPath)
err = osutils.Move(pkgPath, filepath.Join(wd, name))
if err != nil {
- slog.Error(gotext.Get("Error moving the package"), "err", err)
- os.Exit(1)
+ return cliutils.FormatCliExit(gotext.Get("Error moving the package"), err)
}
}
+ slog.Info(gotext.Get("Done"))
+
return nil
},
}
diff --git a/e2e-tests/addrepo_test.go b/e2e-tests/addrepo_test.go
index be41bcf..5e68ae3 100644
--- a/e2e-tests/addrepo_test.go
+++ b/e2e-tests/addrepo_test.go
@@ -33,6 +33,7 @@ func TestE2EAlrAddRepo(t *testing.T) {
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
err := r.Exec(e2e.NewCommand(
+ "sudo",
"alr",
"addrepo",
"--name",
@@ -45,11 +46,12 @@ func TestE2EAlrAddRepo(t *testing.T) {
err = r.Exec(e2e.NewCommand(
"bash",
"-c",
- "cat $HOME/.config/alr/alr.toml",
+ "cat /etc/alr/alr.toml",
))
assert.NoError(t, err)
err = r.Exec(e2e.NewCommand(
+ "sudo",
"alr",
"removerepo",
"--name",
@@ -61,7 +63,7 @@ func TestE2EAlrAddRepo(t *testing.T) {
err = r.Exec(e2e.NewCommand(
"bash",
"-c",
- "cat $HOME/.config/alr/alr.toml",
+ "cat /etc/alr/alr.toml",
), e2e.WithExecOptionStdout(&buf))
assert.NoError(t, err)
assert.Contains(t, buf.String(), "rootCmd")
diff --git a/e2e-tests/common_test.go b/e2e-tests/common_test.go
index e346a9f..ee02aec 100644
--- a/e2e-tests/common_test.go
+++ b/e2e-tests/common_test.go
@@ -109,7 +109,7 @@ var ALL_SYSTEMS []string = []string{
}
var AUTOREQ_AUTOPROV_SYSTEMS []string = []string{
- "alt-sisyphus",
+ // "alt-sisyphus",
"fedora-41",
}
@@ -157,9 +157,9 @@ func dockerMultipleRun(t *testing.T, name string, ids []string, f func(t *testin
imageId := fmt.Sprintf("alr-testimage-%s", id)
runnable := e.Runnable(dockerName).Init(
e2e.StartOptions{
- Image: imageId,
+ Image: imageId,
Volumes: []string{
- "./alr:/usr/bin/alr",
+ // "./alr:/usr/bin/alr",
},
Privileged: true,
},
diff --git a/e2e-tests/images/Dockerfile.fedora-41 b/e2e-tests/images/Dockerfile.fedora-41
index d213964..f91b28f 100644
--- a/e2e-tests/images/Dockerfile.fedora-41
+++ b/e2e-tests/images/Dockerfile.fedora-41
@@ -1,8 +1,18 @@
FROM fedora:41
-RUN dnf install -y ca-certificates sudo rpm-build
-RUN useradd -m -s /bin/bash alr-user && \
- echo "alr-user ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/alr-user && \
- chmod 0440 /etc/sudoers.d/alr-user
-USER alr-user
-WORKDIR /home/alr-user
+RUN dnf install -y ca-certificates sudo rpm-build bindfs
+RUN <> /etc/sudoers.d/user
+ chmod 0440 /etc/sudoers.d/user
+
+ useradd -m -s /bin/bash alr
+ mkdir -p /var/cache/alr /etc/alr
+ chown alr:alr /var/cache/alr /etc/alr
+EOF
+COPY ./alr /usr/bin
+RUN <> /etc/sudoers.d/alr-user && \
- chmod 0440 /etc/sudoers.d/alr-user
-USER alr-user
+RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates sudo libcap2-bin
+RUN <> /etc/sudoers.d/user
+ chmod 0440 /etc/sudoers.d/user
+
+ useradd -m -s /bin/bash alr
+ mkdir -p /var/cache/alr /etc/alr
+ chown alr:alr /var/cache/alr /etc/alr
+EOF
+COPY ./alr /usr/bin
+RUN <.
+
+package main
+
+import (
+ "bufio"
+ "errors"
+ "fmt"
+ "log/slog"
+ "os"
+ "os/exec"
+ "os/user"
+ "path/filepath"
+ "syscall"
+
+ "github.com/hashicorp/go-hclog"
+ "github.com/hashicorp/go-plugin"
+ "github.com/leonelquinteros/gotext"
+ "github.com/urfave/cli/v2"
+
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
+ appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/build"
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
+)
+
+func InternalBuildCmd() *cli.Command {
+ return &cli.Command{
+ Name: "_internal-safe-script-executor",
+ HideHelp: true,
+ Hidden: true,
+ Action: func(c *cli.Context) error {
+ logger.SetupForGoPlugin()
+
+ slog.Debug("start _internal-safe-script-executor", "uid", syscall.Getuid(), "gid", syscall.Getgid())
+
+ if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
+ return err
+ }
+
+ cfg := config.New()
+ err := cfg.Load()
+ if err != nil {
+ return cliutils.FormatCliExit(gotext.Get("Error loading config"), err)
+ }
+
+ logger := hclog.New(&hclog.LoggerOptions{
+ Name: "plugin",
+ Output: os.Stderr,
+ Level: hclog.Debug,
+ JSONFormat: false,
+ DisableTime: true,
+ })
+
+ plugin.Serve(&plugin.ServeConfig{
+ HandshakeConfig: build.HandshakeConfig,
+ Plugins: map[string]plugin.Plugin{
+ "script-executor": &build.ScriptExecutorPlugin{
+ Impl: build.NewLocalScriptExecutor(cfg),
+ },
+ },
+ Logger: logger,
+ })
+ return nil
+ },
+ }
+}
+
+func InternalInstallCmd() *cli.Command {
+ return &cli.Command{
+ Name: "_internal-installer",
+ HideHelp: true,
+ Hidden: true,
+ Action: func(c *cli.Context) error {
+ logger.SetupForGoPlugin()
+
+ if err := utils.EnsureIsAlrUser(); err != nil {
+ return err
+ }
+
+ // Before escalating the rights, we made sure that
+ // this is an ALR user, so it looks safe.
+ err := utils.EscalateToRootUid()
+ if err != nil {
+ return cliutils.FormatCliExit("cannot escalate to root", err)
+ }
+
+ deps, err := appbuilder.
+ New(c.Context).
+ WithConfig().
+ WithDB().
+ WithReposNoPull().
+ Build()
+ if err != nil {
+ return err
+ }
+ defer deps.Defer()
+
+ logger := hclog.New(&hclog.LoggerOptions{
+ Name: "plugin",
+ Output: os.Stderr,
+ Level: hclog.Trace,
+ JSONFormat: true,
+ DisableTime: true,
+ })
+
+ plugin.Serve(&plugin.ServeConfig{
+ HandshakeConfig: build.HandshakeConfig,
+ Plugins: map[string]plugin.Plugin{
+ "installer": &build.InstallerPlugin{
+ Impl: build.NewInstaller(
+ manager.Detect(),
+ ),
+ },
+ },
+ Logger: logger,
+ })
+ return nil
+ },
+ }
+}
+
+func Mount(target string) (string, func(), error) {
+ exe, err := os.Executable()
+ if err != nil {
+ return "", nil, fmt.Errorf("failed to get executable path: %w", err)
+ }
+
+ cmd := exec.Command(exe, "_internal-temporary-mount", target)
+
+ stdoutPipe, err := cmd.StdoutPipe()
+ if err != nil {
+ return "", nil, fmt.Errorf("failed to get stdout pipe: %w", err)
+ }
+
+ stdinPipe, err := cmd.StdinPipe()
+ if err != nil {
+ return "", nil, fmt.Errorf("failed to get stdin pipe: %w", err)
+ }
+
+ cmd.Stderr = os.Stderr
+
+ if err := cmd.Start(); err != nil {
+ return "", nil, fmt.Errorf("failed to start mount: %w", err)
+ }
+
+ scanner := bufio.NewScanner(stdoutPipe)
+ var mountPath string
+ if scanner.Scan() {
+ mountPath = scanner.Text()
+ }
+
+ if err := scanner.Err(); err != nil {
+ _ = cmd.Process.Kill()
+ return "", nil, fmt.Errorf("failed to read mount output: %w", err)
+ }
+
+ if mountPath == "" {
+ _ = cmd.Process.Kill()
+ return "", nil, errors.New("mount failed: no target path returned")
+ }
+
+ cleanup := func() {
+ slog.Debug("cleanup triggered")
+ _, _ = fmt.Fprintln(stdinPipe, "")
+ _ = cmd.Wait()
+ }
+
+ return mountPath, cleanup, nil
+}
+
+func InternalMountCmd() *cli.Command {
+ return &cli.Command{
+ Name: "_internal-temporary-mount",
+ HideHelp: true,
+ Hidden: true,
+ Action: func(c *cli.Context) error {
+ logger.SetupForGoPlugin()
+
+ sourceDir := c.Args().First()
+
+ u, err := user.Current()
+ if err != nil {
+ return cliutils.FormatCliExit("cannot get current user", err)
+ }
+
+ _, alrGid, err := utils.GetUidGidAlrUser()
+ if err != nil {
+ return cliutils.FormatCliExit("cannot get alr user", err)
+ }
+
+ if _, err := os.Stat(sourceDir); err != nil {
+ return cliutils.FormatCliExit(fmt.Sprintf("cannot read %s", sourceDir), err)
+ }
+
+ if err := utils.EnuseIsPrivilegedGroupMember(); err != nil {
+ return err
+ }
+
+ // Before escalating the rights, we made sure that
+ // 1. user in wheel group
+ // 2. user can access sourceDir
+ if err := utils.EscalateToRootUid(); err != nil {
+ return err
+ }
+ if err := syscall.Setgid(alrGid); err != nil {
+ return err
+ }
+
+ if err := os.MkdirAll(constants.AlrRunDir, 0o770); err != nil {
+ return cliutils.FormatCliExit(fmt.Sprintf("failed to create %s", constants.AlrRunDir), err)
+ }
+
+ if err := os.Chown(constants.AlrRunDir, 0, alrGid); err != nil {
+ return cliutils.FormatCliExit(fmt.Sprintf("failed to chown %s", constants.AlrRunDir), err)
+ }
+
+ targetDir := filepath.Join(constants.AlrRunDir, fmt.Sprintf("bindfs-%d", os.Getpid()))
+ // 0750: owner (root) and group (alr)
+ if err := os.MkdirAll(targetDir, 0o750); err != nil {
+ return cliutils.FormatCliExit("error creating bindfs target directory", err)
+ }
+
+ // chown AlrRunDir/mounts/bindfs-* to (root:alr),
+ // so alr user can access dir
+ if err := os.Chown(targetDir, 0, alrGid); err != nil {
+ return cliutils.FormatCliExit("failed to chown bindfs directory", err)
+ }
+
+ bindfsCmd := exec.Command(
+ "bindfs",
+ fmt.Sprintf("--map=%s/alr:@%s/@alr", u.Uid, u.Gid),
+ sourceDir,
+ targetDir,
+ )
+
+ bindfsCmd.Stderr = os.Stderr
+
+ if err := bindfsCmd.Run(); err != nil {
+ return cliutils.FormatCliExit("failed to strart bindfs", err)
+ }
+
+ fmt.Println(targetDir)
+
+ _, _ = bufio.NewReader(os.Stdin).ReadString('\n')
+
+ slog.Debug("start unmount", "dir", targetDir)
+
+ umountCmd := exec.Command("umount", targetDir)
+ umountCmd.Stderr = os.Stderr
+ if err := umountCmd.Run(); err != nil {
+ return cliutils.FormatCliExit(fmt.Sprintf("failed to unmount %s", targetDir), err)
+ }
+
+ if err := os.Remove(targetDir); err != nil {
+ return cliutils.FormatCliExit(fmt.Sprintf("error removing directory %s", targetDir), err)
+ }
+
+ return nil
+ },
+ }
+}
diff --git a/internal/cliutils/app_builder/builder.go b/internal/cliutils/app_builder/builder.go
new file mode 100644
index 0000000..2989bee
--- /dev/null
+++ b/internal/cliutils/app_builder/builder.go
@@ -0,0 +1,176 @@
+// 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 appbuilder
+
+import (
+ "context"
+ "errors"
+ "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/db"
+ "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 AppDeps struct {
+ Cfg *config.ALRConfig
+ DB *db.Database
+ Repos *repos.Repos
+ Info *distro.OSRelease
+ Manager manager.Manager
+}
+
+func (d *AppDeps) Defer() {
+ if d.DB != nil {
+ if err := d.DB.Close(); err != nil {
+ slog.Warn("failed to close db", "err", err)
+ }
+ }
+}
+
+type AppBuilder struct {
+ deps AppDeps
+ err error
+ ctx context.Context
+}
+
+func New(ctx context.Context) *AppBuilder {
+ return &AppBuilder{ctx: ctx}
+}
+
+func (b *AppBuilder) UseConfig(cfg *config.ALRConfig) *AppBuilder {
+ if b.err != nil {
+ return b
+ }
+ b.deps.Cfg = cfg
+ return b
+}
+
+func (b *AppBuilder) WithConfig() *AppBuilder {
+ if b.err != nil {
+ return b
+ }
+
+ cfg := config.New()
+ if err := cfg.Load(); err != nil {
+ b.err = cliutils.FormatCliExit(gotext.Get("Error loading config"), err)
+ return b
+ }
+
+ b.deps.Cfg = cfg
+ return b
+}
+
+func (b *AppBuilder) WithDB() *AppBuilder {
+ if b.err != nil {
+ return b
+ }
+
+ cfg := b.deps.Cfg
+ if cfg == nil {
+ b.err = errors.New("config is required before initializing DB")
+ return b
+ }
+
+ db := db.New(cfg)
+ if err := db.Init(b.ctx); err != nil {
+ b.err = cliutils.FormatCliExit(gotext.Get("Error initialization database"), err)
+ return b
+ }
+
+ b.deps.DB = db
+ return b
+}
+
+func (b *AppBuilder) WithRepos() *AppBuilder {
+ b.withRepos(true, false)
+ return b
+}
+
+func (b *AppBuilder) WithReposForcePull() *AppBuilder {
+ b.withRepos(true, true)
+ return b
+}
+
+func (b *AppBuilder) WithReposNoPull() *AppBuilder {
+ b.withRepos(false, false)
+ return b
+}
+
+func (b *AppBuilder) withRepos(enablePull, forcePull bool) *AppBuilder {
+ if b.err != nil {
+ return b
+ }
+
+ cfg := b.deps.Cfg
+ db := b.deps.DB
+ if cfg == nil || db == nil {
+ b.err = errors.New("config and db are required before initializing repos")
+ return b
+ }
+
+ rs := repos.New(cfg, db)
+
+ if enablePull && (forcePull || cfg.AutoPull()) {
+ if err := rs.Pull(b.ctx, cfg.Repos()); err != nil {
+ b.err = cliutils.FormatCliExit(gotext.Get("Error pulling repositories"), err)
+ return b
+ }
+ }
+
+ b.deps.Repos = rs
+
+ return b
+}
+
+func (b *AppBuilder) WithDistroInfo() *AppBuilder {
+ if b.err != nil {
+ return b
+ }
+
+ b.deps.Info, b.err = distro.ParseOSRelease(b.ctx)
+ if b.err != nil {
+ b.err = cliutils.FormatCliExit(gotext.Get("Error parsing os release"), b.err)
+ }
+
+ return b
+}
+
+func (b *AppBuilder) WithManager() *AppBuilder {
+ if b.err != nil {
+ return b
+ }
+
+ b.deps.Manager = manager.Detect()
+ if b.deps.Manager == nil {
+ b.err = cliutils.FormatCliExit(gotext.Get("Unable to detect a supported package manager on the system"), nil)
+ }
+
+ return b
+}
+
+func (b *AppBuilder) Build() (*AppDeps, error) {
+ if b.err != nil {
+ return nil, b.err
+ }
+ return &b.deps, nil
+}
diff --git a/internal/cliutils/utils.go b/internal/cliutils/utils.go
new file mode 100644
index 0000000..afd7a04
--- /dev/null
+++ b/internal/cliutils/utils.go
@@ -0,0 +1,60 @@
+// 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 cliutils
+
+import (
+ "errors"
+ "fmt"
+ "log/slog"
+
+ "github.com/urfave/cli/v2"
+)
+
+type BashCompleteWithErrorFunc func(c *cli.Context) error
+
+func BashCompleteWithError(f BashCompleteWithErrorFunc) cli.BashCompleteFunc {
+ return func(c *cli.Context) { HandleExitCoder(f(c)) }
+}
+
+func HandleExitCoder(err error) {
+ if err == nil {
+ return
+ }
+
+ if exitErr, ok := err.(cli.ExitCoder); ok {
+ if err.Error() != "" {
+ if _, ok := exitErr.(cli.ErrorFormatter); ok {
+ slog.Error(fmt.Sprintf("%+v\n", err))
+ } else {
+ slog.Error(err.Error())
+ }
+ }
+ cli.OsExiter(exitErr.ExitCode())
+ return
+ }
+}
+
+func FormatCliExit(msg string, err error) cli.ExitCoder {
+ return FormatCliExitWithCode(msg, err, 1)
+}
+
+func FormatCliExitWithCode(msg string, err error, exitCode int) cli.ExitCoder {
+ if err == nil {
+ return cli.Exit(errors.New(msg), exitCode)
+ }
+ return cli.Exit(fmt.Errorf("%s: %w", msg, err), exitCode)
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index 4643aa0..9fc7b46 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -26,9 +26,9 @@ import (
"reflect"
"github.com/caarlos0/env"
- "github.com/leonelquinteros/gotext"
"github.com/pelletier/go-toml/v2"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
)
@@ -84,34 +84,18 @@ func mergeStructs(dst, src interface{}) {
}
}
-const systemConfigPath = "/etc/alr/alr.toml"
-
func (c *ALRConfig) Load() error {
systemConfig, err := readConfig(
- systemConfigPath,
+ constants.SystemConfigPath,
)
if err != nil {
slog.Debug("Cannot read system config", "err", err)
}
- cfgDir, err := os.UserConfigDir()
- if err != nil {
- slog.Debug("Cannot read user config directory")
- }
- userConfigPath := filepath.Join(cfgDir, "alr", "alr.toml")
-
- userConfig, err := readConfig(
- userConfigPath,
- )
- if err != nil {
- slog.Debug("Cannot read user config")
- }
-
config := &types.Config{}
mergeStructs(config, defaultConfig)
mergeStructs(config, systemConfig)
- mergeStructs(config, userConfig)
err = env.Parse(config)
if err != nil {
return err
@@ -119,17 +103,13 @@ func (c *ALRConfig) Load() error {
c.cfg = config
- cacheDir, err := os.UserCacheDir()
- if err != nil {
- return err
- }
c.paths = &Paths{}
- c.paths.UserConfigPath = userConfigPath
- c.paths.CacheDir = filepath.Join(cacheDir, "alr")
+ c.paths.UserConfigPath = constants.SystemConfigPath
+ c.paths.CacheDir = constants.SystemCachePath
c.paths.RepoDir = filepath.Join(c.paths.CacheDir, "repo")
c.paths.PkgsDir = filepath.Join(c.paths.CacheDir, "pkgs")
c.paths.DBPath = filepath.Join(c.paths.CacheDir, "db")
- c.initPaths()
+ // c.initPaths()
return nil
}
@@ -146,10 +126,6 @@ func (c *ALRConfig) AutoPull() bool {
return c.cfg.AutoPull
}
-func (c *ALRConfig) AllowRunAsRoot() bool {
- return c.cfg.Unsafe.AllowRunAsRoot
-}
-
func (c *ALRConfig) Repos() []types.Repo {
return c.cfg.Repos
}
@@ -170,26 +146,6 @@ func (c *ALRConfig) GetPaths() *Paths {
return c.paths
}
-func (c *ALRConfig) initPaths() {
- err := os.MkdirAll(filepath.Dir(c.paths.UserConfigPath), 0o755)
- if err != nil {
- slog.Error(gotext.Get("Unable to create config directory"), "err", err)
- os.Exit(1)
- }
-
- err = os.MkdirAll(c.paths.RepoDir, 0o755)
- if err != nil {
- slog.Error(gotext.Get("Unable to create repo cache directory"), "err", err)
- os.Exit(1)
- }
-
- err = os.MkdirAll(c.paths.PkgsDir, 0o755)
- if err != nil {
- slog.Error(gotext.Get("Unable to create package cache directory"), "err", err)
- os.Exit(1)
- }
-}
-
func (c *ALRConfig) SaveUserConfig() error {
f, err := os.Create(c.paths.UserConfigPath)
if err != nil {
diff --git a/internal/constants/constants.go b/internal/constants/constants.go
new file mode 100644
index 0000000..f113d59
--- /dev/null
+++ b/internal/constants/constants.go
@@ -0,0 +1,24 @@
+// 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 constants
+
+const (
+ SystemConfigPath = "/etc/alr/alr.toml"
+ SystemCachePath = "/var/cache/alr"
+ AlrRunDir = "/var/run/alr"
+ PrivilegedGroup = "wheel"
+)
diff --git a/internal/logger/hclog.go b/internal/logger/hclog.go
new file mode 100644
index 0000000..508e8dd
--- /dev/null
+++ b/internal/logger/hclog.go
@@ -0,0 +1,152 @@
+// 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 logger
+
+import (
+ "io"
+ "log"
+ "strings"
+
+ chLog "github.com/charmbracelet/log"
+ "github.com/hashicorp/go-hclog"
+)
+
+type HCLoggerAdapter struct {
+ logger *Logger
+}
+
+func hclogLevelTochLog(level hclog.Level) chLog.Level {
+ switch level {
+ case hclog.Debug:
+ return chLog.DebugLevel
+ case hclog.Info:
+ return chLog.InfoLevel
+ case hclog.Warn:
+ return chLog.WarnLevel
+ case hclog.Error:
+ return chLog.ErrorLevel
+ }
+ return chLog.FatalLevel
+}
+
+func (a *HCLoggerAdapter) Log(level hclog.Level, msg string, args ...interface{}) {
+ filteredArgs := make([]interface{}, 0, len(args))
+ for i := 0; i < len(args); i += 2 {
+ if i+1 >= len(args) {
+ filteredArgs = append(filteredArgs, args[i])
+ continue
+ }
+
+ key, ok := args[i].(string)
+ if !ok || key != "timestamp" {
+ filteredArgs = append(filteredArgs, args[i], args[i+1])
+ }
+ }
+
+ // Start ugly hacks
+ // Ignore exit messages
+ // - https://github.com/hashicorp/go-plugin/issues/331
+ // - https://github.com/hashicorp/go-plugin/issues/203
+ // - https://github.com/hashicorp/go-plugin/issues/192
+ var chLogLevel chLog.Level
+ if msg == "plugin process exited" ||
+ strings.HasPrefix(msg, "[ERR] plugin: stream copy 'stderr' error") ||
+ strings.HasPrefix(msg, "[DEBUG] plugin") {
+ chLogLevel = chLog.DebugLevel
+ } else {
+ chLogLevel = hclogLevelTochLog(level)
+ }
+
+ a.logger.l.Log(chLogLevel, msg, filteredArgs...)
+}
+
+func (a *HCLoggerAdapter) Trace(msg string, args ...interface{}) {
+ a.Log(hclog.Trace, msg, args...)
+}
+
+func (a *HCLoggerAdapter) Debug(msg string, args ...interface{}) {
+ a.Log(hclog.Debug, msg, args...)
+}
+
+func (a *HCLoggerAdapter) Info(msg string, args ...interface{}) {
+ a.Log(hclog.Info, msg, args...)
+}
+
+func (a *HCLoggerAdapter) Warn(msg string, args ...interface{}) {
+ a.Log(hclog.Warn, msg, args...)
+}
+
+func (a *HCLoggerAdapter) Error(msg string, args ...interface{}) {
+ a.Log(hclog.Error, msg, args...)
+}
+
+func (a *HCLoggerAdapter) IsTrace() bool {
+ return a.logger.l.GetLevel() <= chLog.DebugLevel
+}
+
+func (a *HCLoggerAdapter) IsDebug() bool {
+ return a.logger.l.GetLevel() <= chLog.DebugLevel
+}
+
+func (a *HCLoggerAdapter) IsInfo() bool {
+ return a.logger.l.GetLevel() <= chLog.InfoLevel
+}
+
+func (a *HCLoggerAdapter) IsWarn() bool {
+ return a.logger.l.GetLevel() <= chLog.WarnLevel
+}
+
+func (a *HCLoggerAdapter) IsError() bool {
+ return a.logger.l.GetLevel() <= chLog.ErrorLevel
+}
+
+func (a *HCLoggerAdapter) ImpliedArgs() []interface{} {
+ return nil
+}
+
+func (a *HCLoggerAdapter) With(args ...interface{}) hclog.Logger {
+ return a
+}
+
+func (a *HCLoggerAdapter) Name() string {
+ return ""
+}
+
+func (a *HCLoggerAdapter) Named(name string) hclog.Logger {
+ return a
+}
+
+func (a *HCLoggerAdapter) ResetNamed(name string) hclog.Logger {
+ return a
+}
+
+func (a *HCLoggerAdapter) SetLevel(level hclog.Level) {
+}
+
+func (a *HCLoggerAdapter) StandardLogger(opts *hclog.StandardLoggerOptions) *log.Logger {
+ return nil
+}
+
+func (a *HCLoggerAdapter) StandardWriter(opts *hclog.StandardLoggerOptions) io.Writer {
+ return nil
+}
+
+func GetHCLoggerAdapter() *HCLoggerAdapter {
+ return &HCLoggerAdapter{
+ logger: logger,
+ }
+}
diff --git a/internal/logger/log.go b/internal/logger/log.go
index cd52266..0073a89 100644
--- a/internal/logger/log.go
+++ b/internal/logger/log.go
@@ -22,96 +22,90 @@ import (
"os"
"github.com/charmbracelet/lipgloss"
- "github.com/charmbracelet/log"
+
+ chLog "github.com/charmbracelet/log"
"github.com/leonelquinteros/gotext"
)
type Logger struct {
- lOut slog.Handler
- lErr slog.Handler
+ l *chLog.Logger
}
-func setupOutLogger() *log.Logger {
- styles := log.DefaultStyles()
- logger := log.New(os.Stdout)
- styles.Levels[log.InfoLevel] = lipgloss.NewStyle().
+func setupLogger() *chLog.Logger {
+ styles := chLog.DefaultStyles()
+ logger := chLog.New(os.Stderr)
+ styles.Levels[chLog.InfoLevel] = lipgloss.NewStyle().
SetString("-->").
Foreground(lipgloss.Color("35"))
- logger.SetStyles(styles)
- return logger
-}
-
-func setupErrorLogger() *log.Logger {
- styles := log.DefaultStyles()
- styles.Levels[log.ErrorLevel] = lipgloss.NewStyle().
+ styles.Levels[chLog.ErrorLevel] = lipgloss.NewStyle().
SetString(gotext.Get("ERROR")).
Padding(0, 1, 0, 1).
Background(lipgloss.Color("204")).
Foreground(lipgloss.Color("0"))
- logger := log.New(os.Stderr)
logger.SetStyles(styles)
return logger
}
func New() *Logger {
- standardLogger := setupOutLogger()
- errLogger := setupErrorLogger()
return &Logger{
- lOut: standardLogger,
- lErr: errLogger,
+ l: setupLogger(),
}
}
-func slogLevelToLog(level slog.Level) log.Level {
+func slogLevelToLog(level slog.Level) chLog.Level {
switch level {
case slog.LevelDebug:
- return log.DebugLevel
+ return chLog.DebugLevel
case slog.LevelInfo:
- return log.InfoLevel
+ return chLog.InfoLevel
case slog.LevelWarn:
- return log.WarnLevel
+ return chLog.WarnLevel
case slog.LevelError:
- return log.ErrorLevel
+ return chLog.ErrorLevel
}
- return log.FatalLevel
+ return chLog.FatalLevel
}
func (l *Logger) SetLevel(level slog.Level) {
- l.lOut.(*log.Logger).SetLevel(slogLevelToLog(level))
- l.lErr.(*log.Logger).SetLevel(slogLevelToLog(level))
+ l.l.SetLevel(slogLevelToLog(level))
}
func (l *Logger) Enabled(ctx context.Context, level slog.Level) bool {
- if level <= slog.LevelInfo {
- return l.lOut.Enabled(ctx, level)
- }
- return l.lErr.Enabled(ctx, level)
+ return l.l.Enabled(ctx, level)
}
func (l *Logger) Handle(ctx context.Context, rec slog.Record) error {
- if rec.Level <= slog.LevelInfo {
- return l.lOut.Handle(ctx, rec)
- }
- return l.lErr.Handle(ctx, rec)
+ return l.l.Handle(ctx, rec)
}
func (l *Logger) WithAttrs(attrs []slog.Attr) slog.Handler {
sl := *l
- sl.lOut = l.lOut.WithAttrs(attrs)
- sl.lErr = l.lErr.WithAttrs(attrs)
+ sl.l = l.l.WithAttrs(attrs).(*chLog.Logger)
return &sl
}
func (l *Logger) WithGroup(name string) slog.Handler {
sl := *l
- sl.lOut = l.lOut.WithGroup(name)
- sl.lErr = l.lErr.WithGroup(name)
+ sl.l = l.l.WithGroup(name).(*chLog.Logger)
return &sl
}
+var logger *Logger
+
func SetupDefault() *Logger {
- l := New()
- logger := slog.New(l)
- slog.SetDefault(logger)
- return l
+ logger = New()
+ slogLogger := slog.New(logger)
+ slog.SetDefault(slogLogger)
+ return logger
+}
+
+func SetupForGoPlugin() {
+ logger.l.SetFormatter(chLog.JSONFormatter)
+ chLog.TimestampKey = "@timestamp"
+ chLog.MessageKey = "@message"
+ chLog.LevelKey = "@level"
+}
+
+func GetLogger() *Logger {
+ return logger
}
diff --git a/internal/shutils/handlers/fakeroot.go b/internal/shutils/handlers/fakeroot.go
index bc0090b..90a45dc 100644
--- a/internal/shutils/handlers/fakeroot.go
+++ b/internal/shutils/handlers/fakeroot.go
@@ -25,11 +25,11 @@ import (
"os"
"os/exec"
"runtime"
+ "slices"
"strings"
"syscall"
"time"
- "gitea.plemya-x.ru/Plemya-x/fakeroot"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
)
@@ -54,7 +54,7 @@ func FakerootExecHandler(killTimeout time.Duration) interp.ExecHandlerFunc {
Stderr: hc.Stderr,
}
- err = fakeroot.Apply(cmd)
+ err = Apply(cmd)
if err != nil {
return err
}
@@ -108,6 +108,52 @@ func FakerootExecHandler(killTimeout time.Duration) interp.ExecHandlerFunc {
}
}
+func rootMap(m syscall.SysProcIDMap) bool {
+ return m.ContainerID == 0
+}
+
+func Apply(cmd *exec.Cmd) error {
+ uid := os.Getuid()
+ gid := os.Getgid()
+
+ // If the user is already root, there's no need for fakeroot
+ if uid == 0 {
+ return nil
+ }
+
+ // Ensure SysProcAttr isn't nil
+ if cmd.SysProcAttr == nil {
+ cmd.SysProcAttr = &syscall.SysProcAttr{}
+ }
+
+ // Create a new user namespace
+ cmd.SysProcAttr.Cloneflags |= syscall.CLONE_NEWUSER
+
+ // If the command already contains a mapping for the root user, return an error
+ if slices.ContainsFunc(cmd.SysProcAttr.UidMappings, rootMap) {
+ return nil
+ }
+
+ // If the command already contains a mapping for the root group, return an error
+ if slices.ContainsFunc(cmd.SysProcAttr.GidMappings, rootMap) {
+ return nil
+ }
+
+ cmd.SysProcAttr.UidMappings = append(cmd.SysProcAttr.UidMappings, syscall.SysProcIDMap{
+ ContainerID: 0,
+ HostID: uid,
+ Size: 1,
+ })
+
+ cmd.SysProcAttr.GidMappings = append(cmd.SysProcAttr.GidMappings, syscall.SysProcIDMap{
+ ContainerID: 0,
+ HostID: gid,
+ Size: 1,
+ })
+
+ return nil
+}
+
// execEnv was extracted from github.com/mvdan/sh/interp/vars.go
func execEnv(env expand.Environ) []string {
list := make([]string, 0, 64)
diff --git a/internal/translations/default.pot b/internal/translations/default.pot
index 9247f0d..cd23fb8 100644
--- a/internal/translations/default.pot
+++ b/internal/translations/default.pot
@@ -9,91 +9,83 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
-#: build.go:44
+#: build.go:42
msgid "Build a local package"
msgstr ""
-#: build.go:50
+#: build.go:48
msgid "Path to the build script"
msgstr ""
-#: build.go:55
+#: build.go:53
msgid "Specify subpackage in script (for multi package script only)"
msgstr ""
-#: build.go:60
+#: build.go:58
msgid "Name of the package to build and its repo (example: default/go-bin)"
msgstr ""
-#: build.go:65
+#: build.go:63
msgid ""
"Build package from scratch even if there's an already built package available"
msgstr ""
#: build.go:73
-msgid "Error loading config"
-msgstr ""
-
-#: build.go:81
-msgid "Error initialization database"
-msgstr ""
-
-#: build.go:110
-msgid "Package not found"
-msgstr ""
-
-#: build.go:130
-msgid "Error pulling repositories"
-msgstr ""
-
-#: build.go:138
-msgid "Unable to detect a supported package manager on the system"
-msgstr ""
-
-#: build.go:144
-msgid "Error parsing os release"
-msgstr ""
-
-#: build.go:166
-msgid "Error building package"
-msgstr ""
-
-#: build.go:173
msgid "Error getting working directory"
msgstr ""
-#: build.go:182
+#: build.go:118
+msgid "Cannot get absolute script path"
+msgstr ""
+
+#: build.go:148
+msgid "Package not found"
+msgstr ""
+
+#: build.go:161
+msgid "Nothing to build"
+msgstr ""
+
+#: build.go:218
+msgid "Error building package"
+msgstr ""
+
+#: build.go:225
msgid "Error moving the package"
msgstr ""
-#: fix.go:37
+#: build.go:229
+msgid "Done"
+msgstr ""
+
+#: fix.go:38
msgid "Attempt to fix problems with ALR"
msgstr ""
-#: fix.go:49
-msgid "Removing cache directory"
+#: fix.go:59
+msgid "Clearing cache directory"
msgstr ""
-#: fix.go:53
-msgid "Unable to remove cache directory"
+#: fix.go:64
+msgid "Unable to open cache directory"
msgstr ""
-#: fix.go:57
+#: fix.go:70
+msgid "Unable to read cache directory contents"
+msgstr ""
+
+#: fix.go:76
+msgid "Unable to remove cache item (%s)"
+msgstr ""
+
+#: fix.go:80
msgid "Rebuilding cache"
msgstr ""
-#: fix.go:61
+#: fix.go:84
msgid "Unable to create new cache directory"
msgstr ""
-#: fix.go:81
-msgid "Error pulling repos"
-msgstr ""
-
-#: fix.go:85
-msgid "Done"
-msgstr ""
-
#: gen.go:34
msgid "Generate a ALR script from a template"
msgstr ""
@@ -102,82 +94,106 @@ msgstr ""
msgid "Generate a ALR script for a pip module"
msgstr ""
-#: helper.go:41
+#: helper.go:42
msgid "List all the available helper commands"
msgstr ""
-#: helper.go:53
+#: helper.go:54
msgid "Run a ALR helper command"
msgstr ""
-#: helper.go:60
+#: helper.go:61
msgid "The directory that the install commands will install to"
msgstr ""
-#: helper.go:73
+#: helper.go:74 helper.go:75
msgid "No such helper command"
msgstr ""
-#: info.go:43
-msgid "Print information about a package"
-msgstr ""
-
-#: info.go:48
-msgid "Show all information, not just for the current distro"
-msgstr ""
-
-#: info.go:69
-msgid "Error getting packages"
-msgstr ""
-
-#: info.go:78
-msgid "Error iterating over packages"
-msgstr ""
-
-#: info.go:105
-msgid "Command info expected at least 1 argument, got %d"
-msgstr ""
-
-#: info.go:119
-msgid "Error finding packages"
-msgstr ""
-
-#: info.go:144
+#: helper.go:85
msgid "Error parsing os-release file"
msgstr ""
-#: info.go:153
+#: info.go:42
+msgid "Print information about a package"
+msgstr ""
+
+#: info.go:47
+msgid "Show all information, not just for the current distro"
+msgstr ""
+
+#: info.go:68
+msgid "Error getting packages"
+msgstr ""
+
+#: info.go:76
+msgid "Error iterating over packages"
+msgstr ""
+
+#: info.go:90
+msgid "Command info expected at least 1 argument, got %d"
+msgstr ""
+
+#: info.go:110
+msgid "Error finding packages"
+msgstr ""
+
+#: info.go:124
+msgid "Can't detect system language"
+msgstr ""
+
+#: info.go:141
msgid "Error resolving overrides"
msgstr ""
-#: info.go:162 info.go:168
+#: info.go:149 info.go:154
msgid "Error encoding script variables"
msgstr ""
-#: install.go:43
+#: install.go:40
msgid "Install a new package"
msgstr ""
-#: install.go:57
+#: install.go:56
msgid "Command install expected at least 1 argument, got %d"
msgstr ""
+#: install.go:118
+msgid "Error parsing os release"
+msgstr ""
+
#: install.go:163
msgid "Remove an installed package"
msgstr ""
-#: install.go:188
+#: install.go:182
msgid "Error listing installed packages"
msgstr ""
-#: install.go:226
+#: install.go:223
msgid "Command remove expected at least 1 argument, got %d"
msgstr ""
-#: install.go:241
+#: install.go:238
msgid "Error removing packages"
msgstr ""
+#: internal/cliutils/app_builder/builder.go:75
+msgid "Error loading config"
+msgstr ""
+
+#: internal/cliutils/app_builder/builder.go:96
+msgid "Error initialization database"
+msgstr ""
+
+#: internal/cliutils/app_builder/builder.go:135
+msgid "Error pulling repositories"
+msgstr ""
+
+#: internal/cliutils/app_builder/builder.go:165
+msgid "Unable to detect a supported package manager on the system"
+msgstr ""
+
#: internal/cliutils/prompt.go:60
msgid "Would you like to view the build script for %s"
msgstr ""
@@ -258,18 +274,6 @@ msgstr ""
msgid "OPTIONS"
msgstr ""
-#: internal/config/config.go:176
-msgid "Unable to create config directory"
-msgstr ""
-
-#: internal/config/config.go:182
-msgid "Unable to create repo cache directory"
-msgstr ""
-
-#: internal/config/config.go:188
-msgid "Unable to create package cache directory"
-msgstr ""
-
#: internal/db/db.go:133
msgid "Database version mismatch; resetting"
msgstr ""
@@ -303,10 +307,22 @@ msgstr ""
msgid "%s %s downloading at %s/s\n"
msgstr ""
-#: internal/logger/log.go:47
+#: internal/logger/log.go:41
msgid "ERROR"
msgstr ""
+#: internal/utils/cmd.go:95
+msgid "Error dropping capabilities"
+msgstr ""
+
+#: internal/utils/cmd.go:123
+msgid "You need to be root to perform this action"
+msgstr ""
+
+#: internal/utils/cmd.go:165
+msgid "You need to be a %s member to perform this action"
+msgstr ""
+
#: list.go:41
msgid "List ALR repo packages"
msgstr ""
@@ -323,86 +339,40 @@ msgstr ""
msgid "Enable interactive questions and prompts"
msgstr ""
-#: main.go:96
-msgid ""
-"Running ALR as root is forbidden as it may cause catastrophic damage to your "
-"system"
-msgstr ""
-
-#: main.go:154
+#: main.go:145
msgid "Show help"
msgstr ""
-#: main.go:158
+#: main.go:149
msgid "Error while running app"
msgstr ""
-#: pkg/build/build.go:157
-msgid "Failed to prompt user to view build script"
-msgstr ""
-
-#: pkg/build/build.go:161
+#: pkg/build/build.go:394
msgid "Building package"
msgstr ""
-#: pkg/build/build.go:209
+#: pkg/build/build.go:423
msgid "The checksums array must be the same length as sources"
msgstr ""
-#: pkg/build/build.go:238
+#: pkg/build/build.go:454
msgid "Downloading sources"
msgstr ""
-#: pkg/build/build.go:260
-msgid "Building package metadata"
+#: pkg/build/build.go:543
+msgid "Installing dependencies"
msgstr ""
-#: pkg/build/build.go:282
-msgid "Compressing package"
-msgstr ""
-
-#: pkg/build/build.go:441
+#: pkg/build/checker.go:43
msgid ""
"Your system's CPU architecture doesn't match this package. Do you want to "
"build anyway?"
msgstr ""
-#: pkg/build/build.go:455
+#: pkg/build/checker.go:67
msgid "This package is already installed"
msgstr ""
-#: pkg/build/build.go:479
-msgid "Installing build dependencies"
-msgstr ""
-
-#: pkg/build/build.go:524
-msgid "Installing dependencies"
-msgstr ""
-
-#: pkg/build/build.go:605
-msgid "Would you like to remove the build dependencies?"
-msgstr ""
-
-#: pkg/build/build.go:668
-msgid "Executing prepare()"
-msgstr ""
-
-#: pkg/build/build.go:678
-msgid "Executing build()"
-msgstr ""
-
-#: pkg/build/build.go:708 pkg/build/build.go:728
-msgid "Executing %s()"
-msgstr ""
-
-#: pkg/build/build.go:787
-msgid "Error installing native packages"
-msgstr ""
-
-#: pkg/build/build.go:811
-msgid "Error installing package"
-msgstr ""
-
#: pkg/build/find_deps/alt_linux.go:35
msgid "Command not found on the system"
msgstr ""
@@ -423,6 +393,22 @@ msgstr ""
msgid "AutoReq is not implemented for this package format, so it's skipped"
msgstr ""
+#: pkg/build/script_executor.go:237
+msgid "Building package metadata"
+msgstr ""
+
+#: pkg/build/script_executor.go:356
+msgid "Executing prepare()"
+msgstr ""
+
+#: pkg/build/script_executor.go:365
+msgid "Executing build()"
+msgstr ""
+
+#: pkg/build/script_executor.go:394 pkg/build/script_executor.go:414
+msgid "Executing %s()"
+msgstr ""
+
#: pkg/repos/pull.go:79
msgid "Pulling repository"
msgstr ""
@@ -441,43 +427,47 @@ msgid ""
"updating ALR if something doesn't work."
msgstr ""
-#: repo.go:40
+#: repo.go:39
msgid "Add a new repository"
msgstr ""
-#: repo.go:47
+#: repo.go:46
msgid "Name of the new repo"
msgstr ""
-#: repo.go:53
+#: repo.go:52
msgid "URL of the new repo"
msgstr ""
-#: repo.go:86 repo.go:156
+#: repo.go:79
+msgid "Repo %s already exists"
+msgstr ""
+
+#: repo.go:90 repo.go:167
msgid "Error saving config"
msgstr ""
-#: repo.go:111
+#: repo.go:116
msgid "Remove an existing repository"
msgstr ""
-#: repo.go:118
+#: repo.go:123
msgid "Name of the repo to be deleted"
msgstr ""
-#: repo.go:142
-msgid "Repo does not exist"
+#: repo.go:156
+msgid "Repo \"%s\" does not exist"
msgstr ""
-#: repo.go:150
+#: repo.go:163
msgid "Error removing repo directory"
msgstr ""
-#: repo.go:167
+#: repo.go:186
msgid "Error removing packages from database"
msgstr ""
-#: repo.go:179
+#: repo.go:197
msgid "Pull all repositories that have changed"
msgstr ""
@@ -505,11 +495,15 @@ msgstr ""
msgid "Format output using a Go template"
msgstr ""
-#: search.go:88 search.go:105
+#: search.go:96
+msgid "Error while executing search"
+msgstr ""
+
+#: search.go:104
msgid "Error parsing format template"
msgstr ""
-#: search.go:113
+#: search.go:112
msgid "Error executing template"
msgstr ""
@@ -517,10 +511,10 @@ msgstr ""
msgid "Upgrade all installed packages"
msgstr ""
-#: upgrade.go:96
+#: upgrade.go:109 upgrade.go:126
msgid "Error checking for updates"
msgstr ""
-#: upgrade.go:118
+#: upgrade.go:129
msgid "There is nothing to do."
msgstr ""
diff --git a/internal/translations/po/ru/default.po b/internal/translations/po/ru/default.po
index 1671e6b..2714217 100644
--- a/internal/translations/po/ru/default.po
+++ b/internal/translations/po/ru/default.po
@@ -16,92 +16,88 @@ msgstr ""
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Gtranslator 47.1\n"
-#: build.go:44
+#: build.go:42
msgid "Build a local package"
msgstr "Сборка локального пакета"
-#: build.go:50
+#: build.go:48
msgid "Path to the build script"
msgstr "Путь к скрипту сборки"
-#: build.go:55
+#: build.go:53
msgid "Specify subpackage in script (for multi package script only)"
msgstr "Укажите подпакет в скрипте (только для многопакетного скрипта)"
-#: build.go:60
+#: build.go:58
msgid "Name of the package to build and its repo (example: default/go-bin)"
msgstr "Имя пакета для сборки и его репозиторий (пример: default/go-bin)"
-#: build.go:65
+#: build.go:63
msgid ""
"Build package from scratch even if there's an already built package available"
msgstr "Создайте пакет с нуля, даже если уже имеется готовый пакет"
#: build.go:73
-#, fuzzy
-msgid "Error loading config"
-msgstr "Ошибка при кодировании конфигурации"
-
-#: build.go:81
-msgid "Error initialization database"
-msgstr "Ошибка инициализации базы данных"
-
-#: build.go:110
-msgid "Package not found"
-msgstr "Пакет не найден"
-
-#: build.go:130
-msgid "Error pulling repositories"
-msgstr "Ошибка при извлечении репозиториев"
-
-#: build.go:138
-msgid "Unable to detect a supported package manager on the system"
-msgstr "Не удалось обнаружить поддерживаемый менеджер пакетов в системе"
-
-#: build.go:144
-msgid "Error parsing os release"
-msgstr "Ошибка при разборе файла выпуска операционной системы"
-
-#: build.go:166
-msgid "Error building package"
-msgstr "Ошибка при сборке пакета"
-
-#: build.go:173
msgid "Error getting working directory"
msgstr "Ошибка при получении рабочего каталога"
-#: build.go:182
+#: build.go:118
+msgid "Cannot get absolute script path"
+msgstr ""
+
+#: build.go:148
+msgid "Package not found"
+msgstr "Пакет не найден"
+
+#: build.go:161
+#, fuzzy
+msgid "Nothing to build"
+msgstr "Исполнение build()"
+
+#: build.go:218
+msgid "Error building package"
+msgstr "Ошибка при сборке пакета"
+
+#: build.go:225
msgid "Error moving the package"
msgstr "Ошибка при перемещении пакета"
-#: fix.go:37
+#: build.go:229
+msgid "Done"
+msgstr "Сделано"
+
+#: fix.go:38
msgid "Attempt to fix problems with ALR"
msgstr "Попытка устранить проблемы с ALR"
-#: fix.go:49
-msgid "Removing cache directory"
+#: fix.go:59
+#, fuzzy
+msgid "Clearing cache directory"
msgstr "Удаление каталога кэша"
-#: fix.go:53
-msgid "Unable to remove cache directory"
+#: fix.go:64
+#, fuzzy
+msgid "Unable to open cache directory"
msgstr "Не удалось удалить каталог кэша"
-#: fix.go:57
+#: fix.go:70
+#, fuzzy
+msgid "Unable to read cache directory contents"
+msgstr "Не удалось удалить каталог кэша"
+
+#: fix.go:76
+#, fuzzy
+msgid "Unable to remove cache item (%s)"
+msgstr "Не удалось удалить каталог кэша"
+
+#: fix.go:80
msgid "Rebuilding cache"
msgstr "Восстановление кэша"
-#: fix.go:61
+#: fix.go:84
msgid "Unable to create new cache directory"
msgstr "Не удалось создать новый каталог кэша"
-#: fix.go:81
-msgid "Error pulling repos"
-msgstr "Ошибка при извлечении репозиториев"
-
-#: fix.go:85
-msgid "Done"
-msgstr "Сделано"
-
#: gen.go:34
msgid "Generate a ALR script from a template"
msgstr "Генерация скрипта ALR из шаблона"
@@ -110,82 +106,108 @@ msgstr "Генерация скрипта ALR из шаблона"
msgid "Generate a ALR script for a pip module"
msgstr "Генерация скрипта ALR для модуля pip"
-#: helper.go:41
+#: helper.go:42
msgid "List all the available helper commands"
msgstr "Список всех доступных вспомогательных команды"
-#: helper.go:53
+#: helper.go:54
msgid "Run a ALR helper command"
msgstr "Запустить вспомогательную команду ALR"
-#: helper.go:60
+#: helper.go:61
msgid "The directory that the install commands will install to"
msgstr "Каталог, в который будут устанавливать команды установки"
-#: helper.go:73
+#: helper.go:74 helper.go:75
msgid "No such helper command"
msgstr "Такой вспомогательной команды нет"
-#: info.go:43
-msgid "Print information about a package"
-msgstr "Отобразить информацию о пакете"
-
-#: info.go:48
-msgid "Show all information, not just for the current distro"
-msgstr "Показывать всю информацию, не только для текущего дистрибутива"
-
-#: info.go:69
-msgid "Error getting packages"
-msgstr "Ошибка при получении пакетов"
-
-#: info.go:78
-msgid "Error iterating over packages"
-msgstr "Ошибка при переборе пакетов"
-
-#: info.go:105
-msgid "Command info expected at least 1 argument, got %d"
-msgstr "Для команды info ожидался хотя бы 1 аргумент, получено %d"
-
-#: info.go:119
-msgid "Error finding packages"
-msgstr "Ошибка при поиске пакетов"
-
-#: info.go:144
+#: helper.go:85
msgid "Error parsing os-release file"
msgstr "Ошибка при разборе файла выпуска операционной системы"
-#: info.go:153
+#: info.go:42
+msgid "Print information about a package"
+msgstr "Отобразить информацию о пакете"
+
+#: info.go:47
+msgid "Show all information, not just for the current distro"
+msgstr "Показывать всю информацию, не только для текущего дистрибутива"
+
+#: info.go:68
+msgid "Error getting packages"
+msgstr "Ошибка при получении пакетов"
+
+#: info.go:76
+msgid "Error iterating over packages"
+msgstr "Ошибка при переборе пакетов"
+
+#: info.go:90
+msgid "Command info expected at least 1 argument, got %d"
+msgstr "Для команды info ожидался хотя бы 1 аргумент, получено %d"
+
+#: info.go:110
+msgid "Error finding packages"
+msgstr "Ошибка при поиске пакетов"
+
+#: info.go:124
+#, fuzzy
+msgid "Can't detect system language"
+msgstr "Ошибка при парсинге языка системы"
+
+#: info.go:141
msgid "Error resolving overrides"
msgstr "Ошибка устранения переорпеделений"
-#: info.go:162 info.go:168
+#: info.go:149 info.go:154
msgid "Error encoding script variables"
msgstr "Ошибка кодирования переменных скрита"
-#: install.go:43
+#: install.go:40
msgid "Install a new package"
msgstr "Установить новый пакет"
-#: install.go:57
+#: install.go:56
msgid "Command install expected at least 1 argument, got %d"
msgstr "Для команды install ожидался хотя бы 1 аргумент, получено %d"
+#: install.go:118
+msgid "Error parsing os release"
+msgstr "Ошибка при разборе файла выпуска операционной системы"
+
#: install.go:163
msgid "Remove an installed package"
msgstr "Удалить установленный пакет"
-#: install.go:188
+#: install.go:182
msgid "Error listing installed packages"
msgstr "Ошибка при составлении списка установленных пакетов"
-#: install.go:226
+#: install.go:223
msgid "Command remove expected at least 1 argument, got %d"
msgstr "Для команды remove ожидался хотя бы 1 аргумент, получено %d"
-#: install.go:241
+#: install.go:238
msgid "Error removing packages"
msgstr "Ошибка при удалении пакетов"
+#: internal/cliutils/app_builder/builder.go:75
+#, fuzzy
+msgid "Error loading config"
+msgstr "Ошибка при кодировании конфигурации"
+
+#: internal/cliutils/app_builder/builder.go:96
+msgid "Error initialization database"
+msgstr "Ошибка инициализации базы данных"
+
+#: internal/cliutils/app_builder/builder.go:135
+msgid "Error pulling repositories"
+msgstr "Ошибка при извлечении репозиториев"
+
+#: internal/cliutils/app_builder/builder.go:165
+msgid "Unable to detect a supported package manager on the system"
+msgstr "Не удалось обнаружить поддерживаемый менеджер пакетов в системе"
+
#: internal/cliutils/prompt.go:60
msgid "Would you like to view the build script for %s"
msgstr "Показать скрипт для пакета %s"
@@ -266,19 +288,6 @@ msgstr "КАТЕГОРИЯ"
msgid "OPTIONS"
msgstr "ПАРАМЕТРЫ"
-#: internal/config/config.go:176
-#, fuzzy
-msgid "Unable to create config directory"
-msgstr "Не удалось создать каталог конфигурации ALR"
-
-#: internal/config/config.go:182
-msgid "Unable to create repo cache directory"
-msgstr "Не удалось создать каталог кэша репозитория"
-
-#: internal/config/config.go:188
-msgid "Unable to create package cache directory"
-msgstr "Не удалось создать каталог кэша пакетов"
-
#: internal/db/db.go:133
msgid "Database version mismatch; resetting"
msgstr "Несоответствие версий базы данных; сброс настроек"
@@ -313,10 +322,23 @@ msgstr "%s: выполнено!\n"
msgid "%s %s downloading at %s/s\n"
msgstr "%s %s загружается — %s/с\n"
-#: internal/logger/log.go:47
+#: internal/logger/log.go:41
msgid "ERROR"
msgstr "ОШИБКА"
+#: internal/utils/cmd.go:95
+#, fuzzy
+msgid "Error dropping capabilities"
+msgstr "Ошибка при открытии базы данных"
+
+#: internal/utils/cmd.go:123
+msgid "You need to be root to perform this action"
+msgstr ""
+
+#: internal/utils/cmd.go:165
+msgid "You need to be a %s member to perform this action"
+msgstr ""
+
#: list.go:41
msgid "List ALR repo packages"
msgstr "Список пакетов репозитория ALR"
@@ -333,47 +355,31 @@ msgstr "Аргументы, которые будут переданы мене
msgid "Enable interactive questions and prompts"
msgstr "Включение интерактивных вопросов и запросов"
-#: main.go:96
-msgid ""
-"Running ALR as root is forbidden as it may cause catastrophic damage to your "
-"system"
-msgstr ""
-"Запуск ALR от имени root запрещён, так как это может привести к "
-"катастрофическому повреждению вашей системы"
-
-#: main.go:154
+#: main.go:145
msgid "Show help"
msgstr "Показать справку"
-#: main.go:158
+#: main.go:149
msgid "Error while running app"
msgstr "Ошибка при запуске приложения"
-#: pkg/build/build.go:157
-msgid "Failed to prompt user to view build script"
-msgstr "Не удалось предложить пользователю просмотреть скрипт сборки"
-
-#: pkg/build/build.go:161
+#: pkg/build/build.go:394
msgid "Building package"
msgstr "Сборка пакета"
-#: pkg/build/build.go:209
+#: pkg/build/build.go:423
msgid "The checksums array must be the same length as sources"
msgstr "Массив контрольных сумм должен быть той же длины, что и источники"
-#: pkg/build/build.go:238
+#: pkg/build/build.go:454
msgid "Downloading sources"
msgstr "Скачивание источников"
-#: pkg/build/build.go:260
-msgid "Building package metadata"
-msgstr "Сборка метаданных пакета"
+#: pkg/build/build.go:543
+msgid "Installing dependencies"
+msgstr "Установка зависимостей"
-#: pkg/build/build.go:282
-msgid "Compressing package"
-msgstr "Сжатие пакета"
-
-#: pkg/build/build.go:441
+#: pkg/build/checker.go:43
msgid ""
"Your system's CPU architecture doesn't match this package. Do you want to "
"build anyway?"
@@ -381,42 +387,10 @@ msgstr ""
"Архитектура процессора вашей системы не соответствует этому пакету. Вы все "
"равно хотите выполнить сборку?"
-#: pkg/build/build.go:455
+#: pkg/build/checker.go:67
msgid "This package is already installed"
msgstr "Этот пакет уже установлен"
-#: pkg/build/build.go:479
-msgid "Installing build dependencies"
-msgstr "Установка зависимостей сборки"
-
-#: pkg/build/build.go:524
-msgid "Installing dependencies"
-msgstr "Установка зависимостей"
-
-#: pkg/build/build.go:605
-msgid "Would you like to remove the build dependencies?"
-msgstr "Хотели бы вы удалить зависимости сборки?"
-
-#: pkg/build/build.go:668
-msgid "Executing prepare()"
-msgstr "Исполнение prepare()"
-
-#: pkg/build/build.go:678
-msgid "Executing build()"
-msgstr "Исполнение build()"
-
-#: pkg/build/build.go:708 pkg/build/build.go:728
-msgid "Executing %s()"
-msgstr "Исполнение %s()"
-
-#: pkg/build/build.go:787
-msgid "Error installing native packages"
-msgstr "Ошибка при установке нативных пакетов"
-
-#: pkg/build/build.go:811
-msgid "Error installing package"
-msgstr "Ошибка при установке пакета"
-
#: pkg/build/find_deps/alt_linux.go:35
msgid "Command not found on the system"
msgstr "Команда не найдена в системе"
@@ -439,6 +413,22 @@ msgid "AutoReq is not implemented for this package format, so it's skipped"
msgstr ""
"AutoReq не реализовано для этого формата пакета, поэтому будет пропущено"
+#: pkg/build/script_executor.go:237
+msgid "Building package metadata"
+msgstr "Сборка метаданных пакета"
+
+#: pkg/build/script_executor.go:356
+msgid "Executing prepare()"
+msgstr "Исполнение prepare()"
+
+#: pkg/build/script_executor.go:365
+msgid "Executing build()"
+msgstr "Исполнение build()"
+
+#: pkg/build/script_executor.go:394 pkg/build/script_executor.go:414
+msgid "Executing %s()"
+msgstr "Исполнение %s()"
+
#: pkg/repos/pull.go:79
msgid "Pulling repository"
msgstr "Скачивание репозитория"
@@ -459,44 +449,50 @@ msgstr ""
"Минимальная версия ALR для ALR-репозитория выше текущей версии. Попробуйте "
"обновить ALR, если что-то не работает."
-#: repo.go:40
+#: repo.go:39
msgid "Add a new repository"
msgstr "Добавить новый репозиторий"
-#: repo.go:47
+#: repo.go:46
msgid "Name of the new repo"
msgstr "Название нового репозитория"
-#: repo.go:53
+#: repo.go:52
msgid "URL of the new repo"
msgstr "URL-адрес нового репозитория"
-#: repo.go:86 repo.go:156
+#: repo.go:79
+#, fuzzy
+msgid "Repo %s already exists"
+msgstr "Репозитория не существует"
+
+#: repo.go:90 repo.go:167
#, fuzzy
msgid "Error saving config"
msgstr "Ошибка при кодировании конфигурации"
-#: repo.go:111
+#: repo.go:116
msgid "Remove an existing repository"
msgstr "Удалить существующий репозиторий"
-#: repo.go:118
+#: repo.go:123
msgid "Name of the repo to be deleted"
msgstr "Название репозитория удалён"
-#: repo.go:142
-msgid "Repo does not exist"
+#: repo.go:156
+#, fuzzy
+msgid "Repo \"%s\" does not exist"
msgstr "Репозитория не существует"
-#: repo.go:150
+#: repo.go:163
msgid "Error removing repo directory"
msgstr "Ошибка при удалении каталога репозитория"
-#: repo.go:167
+#: repo.go:186
msgid "Error removing packages from database"
msgstr "Ошибка при удалении пакетов из базы данных"
-#: repo.go:179
+#: repo.go:197
msgid "Pull all repositories that have changed"
msgstr "Скачать все изменённые репозитории"
@@ -524,11 +520,16 @@ msgstr "Иcкать по provides"
msgid "Format output using a Go template"
msgstr "Формат выходных данных с использованием шаблона Go"
-#: search.go:88 search.go:105
+#: search.go:96
+#, fuzzy
+msgid "Error while executing search"
+msgstr "Ошибка при запуске приложения"
+
+#: search.go:104
msgid "Error parsing format template"
msgstr "Ошибка при разборе шаблона"
-#: search.go:113
+#: search.go:112
msgid "Error executing template"
msgstr "Ошибка при выполнении шаблона"
@@ -536,14 +537,60 @@ msgstr "Ошибка при выполнении шаблона"
msgid "Upgrade all installed packages"
msgstr "Обновить все установленные пакеты"
-#: upgrade.go:96
+#: upgrade.go:109 upgrade.go:126
msgid "Error checking for updates"
msgstr "Ошибка при проверке обновлений"
-#: upgrade.go:118
+#: upgrade.go:129
msgid "There is nothing to do."
msgstr "Здесь нечего делать."
+#~ msgid "Error pulling repos"
+#~ msgstr "Ошибка при извлечении репозиториев"
+
+#, fuzzy
+#~ msgid "Error getting current executable"
+#~ msgstr "Ошибка при получении рабочего каталога"
+
+#, fuzzy
+#~ msgid "Error mounting"
+#~ msgstr "Ошибка при кодировании конфигурации"
+
+#, fuzzy
+#~ msgid "Unable to create config directory"
+#~ msgstr "Не удалось создать каталог конфигурации ALR"
+
+#~ msgid "Unable to create repo cache directory"
+#~ msgstr "Не удалось создать каталог кэша репозитория"
+
+#~ msgid "Unable to create package cache directory"
+#~ msgstr "Не удалось создать каталог кэша пакетов"
+
+#~ msgid ""
+#~ "Running ALR as root is forbidden as it may cause catastrophic damage to "
+#~ "your system"
+#~ msgstr ""
+#~ "Запуск ALR от имени root запрещён, так как это может привести к "
+#~ "катастрофическому повреждению вашей системы"
+
+#~ msgid "Failed to prompt user to view build script"
+#~ msgstr "Не удалось предложить пользователю просмотреть скрипт сборки"
+
+#~ msgid "Compressing package"
+#~ msgstr "Сжатие пакета"
+
+#~ msgid "Installing build dependencies"
+#~ msgstr "Установка зависимостей сборки"
+
+#~ msgid "Would you like to remove the build dependencies?"
+#~ msgstr "Хотели бы вы удалить зависимости сборки?"
+
+#~ msgid "Error installing native packages"
+#~ msgstr "Ошибка при установке нативных пакетов"
+
+#~ msgid "Error installing package"
+#~ msgstr "Ошибка при установке пакета"
+
#~ msgid "Error opening config file, using defaults"
#~ msgstr ""
#~ "Ошибка при открытии конфигурационного файла, используются значения по "
@@ -569,12 +616,6 @@ msgstr "Здесь нечего делать."
#~ msgid "Error opening config file"
#~ msgstr "Ошибка при открытии конфигурационного файла"
-#~ msgid "Error parsing system language"
-#~ msgstr "Ошибка при парсинге языка системы"
-
-#~ msgid "Error opening database"
-#~ msgstr "Ошибка при открытии базы данных"
-
#~ msgid "Executing version()"
#~ msgstr "Исполнение версия()"
diff --git a/internal/types/build.go b/internal/types/build.go
index 51999ef..aadb8e6 100644
--- a/internal/types/build.go
+++ b/internal/types/build.go
@@ -19,13 +19,11 @@
package types
-import "gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
-
type BuildOpts struct {
- Script string
- Repository string
- Packages []string
- Manager manager.Manager
+ // Script string
+ // Repository string
+ // Packages []string
+ // Manager manager.Manager
Clean bool
Interactive bool
}
diff --git a/internal/types/config.go b/internal/types/config.go
index db0b8e1..8edd2df 100644
--- a/internal/types/config.go
+++ b/internal/types/config.go
@@ -25,7 +25,6 @@ type Config struct {
PagerStyle string `toml:"pagerStyle" env:"ALR_PAGER_STYLE"`
IgnorePkgUpdates []string `toml:"ignorePkgUpdates"`
Repos []Repo `toml:"repo"`
- Unsafe Unsafe `toml:"unsafe"`
AutoPull bool `toml:"autoPull" env:"ALR_AUTOPULL"`
LogLevel string `toml:"logLevel" env:"ALR_LOG_LEVEL"`
}
@@ -35,7 +34,3 @@ type Repo struct {
Name string `toml:"name"`
URL string `toml:"url"`
}
-
-type Unsafe struct {
- AllowRunAsRoot bool `toml:"allowRunAsRoot" env:"ALR_UNSAFE_ALLOW_RUN_AS_ROOT"`
-}
diff --git a/internal/utils/cmd.go b/internal/utils/cmd.go
new file mode 100644
index 0000000..455e5fb
--- /dev/null
+++ b/internal/utils/cmd.go
@@ -0,0 +1,186 @@
+// 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 utils
+
+import (
+ "errors"
+ "os"
+ "os/user"
+ "strconv"
+ "syscall"
+
+ "github.com/leonelquinteros/gotext"
+ "github.com/urfave/cli/v2"
+
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
+)
+
+func GetUidGidAlrUserString() (string, string, error) {
+ u, err := user.Lookup("alr")
+ if err != nil {
+ return "", "", err
+ }
+
+ return u.Uid, u.Gid, nil
+}
+
+func GetUidGidAlrUser() (int, int, error) {
+ strUid, strGid, err := GetUidGidAlrUserString()
+ if err != nil {
+ return 0, 0, err
+ }
+
+ uid, err := strconv.Atoi(strUid)
+ if err != nil {
+ return 0, 0, err
+ }
+ gid, err := strconv.Atoi(strGid)
+ if err != nil {
+ return 0, 0, err
+ }
+
+ return uid, gid, nil
+}
+
+func DropCapsToAlrUser() error {
+ uid, gid, err := GetUidGidAlrUser()
+ if err != nil {
+ return err
+ }
+ err = syscall.Setgid(gid)
+ if err != nil {
+ return err
+ }
+ err = syscall.Setuid(uid)
+ if err != nil {
+ return err
+ }
+ return EnsureIsAlrUser()
+}
+
+func ExitIfCantDropGidToAlr() cli.ExitCoder {
+ _, gid, err := GetUidGidAlrUser()
+ if err != nil {
+ return cliutils.FormatCliExit("cannot get gid alr", err)
+ }
+ err = syscall.Setgid(gid)
+ if err != nil {
+ return cliutils.FormatCliExit("cannot get setgid alr", err)
+ }
+ return nil
+}
+
+// ExitIfCantDropCapsToAlrUser attempts to drop capabilities to the already
+// running user. Returns a cli.ExitCoder with an error if the operation fails.
+// See also [ExitIfCantDropCapsToAlrUserNoPrivs] for a version that also applies
+// no-new-privs.
+func ExitIfCantDropCapsToAlrUser() cli.ExitCoder {
+ err := DropCapsToAlrUser()
+ if err != nil {
+ return cliutils.FormatCliExit(gotext.Get("Error dropping capabilities"), err)
+ }
+ return nil
+}
+
+func ExitIfCantSetNoNewPrivs() cli.ExitCoder {
+ if err := NoNewPrivs(); err != nil {
+ return cliutils.FormatCliExit("error no new privs", err)
+ }
+
+ return nil
+}
+
+// ExitIfCantDropCapsToAlrUserNoPrivs combines [ExitIfCantDropCapsToAlrUser] with [ExitIfCantSetNoNewPrivs]
+func ExitIfCantDropCapsToAlrUserNoPrivs() cli.ExitCoder {
+ if err := ExitIfCantDropCapsToAlrUser(); err != nil {
+ return err
+ }
+
+ if err := ExitIfCantSetNoNewPrivs(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func ExitIfNotRoot() error {
+ if os.Getuid() != 0 {
+ return cli.Exit(gotext.Get("You need to be root to perform this action"), 1)
+ }
+ return nil
+}
+
+func EnsureIsAlrUser() error {
+ uid, gid, err := GetUidGidAlrUser()
+ if err != nil {
+ return err
+ }
+ newUid := syscall.Getuid()
+ if newUid != uid {
+ return errors.New("new uid don't matches requested")
+ }
+ newGid := syscall.Getgid()
+ if newGid != gid {
+ return errors.New("new gid don't matches requested")
+ }
+ return nil
+}
+
+func EnuseIsPrivilegedGroupMember() error {
+ currentUser, err := user.Current()
+ if err != nil {
+ return err
+ }
+
+ group, err := user.LookupGroup(constants.PrivilegedGroup)
+ if err != nil {
+ return err
+ }
+
+ groups, err := currentUser.GroupIds()
+ if err != nil {
+ return err
+ }
+
+ for _, gid := range groups {
+ if gid == group.Gid {
+ return nil
+ }
+ }
+ return cliutils.FormatCliExit(gotext.Get("You need to be a %s member to perform this action", constants.PrivilegedGroup), nil)
+}
+
+func EscalateToRootGid() error {
+ return syscall.Setgid(0)
+}
+
+func EscalateToRootUid() error {
+ return syscall.Setuid(0)
+}
+
+func EscalateToRoot() error {
+ err := EscalateToRootUid()
+ if err != nil {
+ return err
+ }
+ err = EscalateToRootGid()
+ if err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/internal/utils/utils.go b/internal/utils/utils.go
new file mode 100644
index 0000000..9c0b8f7
--- /dev/null
+++ b/internal/utils/utils.go
@@ -0,0 +1,23 @@
+// 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 utils
+
+import "golang.org/x/sys/unix"
+
+func NoNewPrivs() error {
+ return unix.Prctl(unix.PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)
+}
diff --git a/list.go b/list.go
index 4af5d6d..1012db7 100644
--- a/list.go
+++ b/list.go
@@ -22,17 +22,17 @@ package main
import (
"fmt"
"log/slog"
- "os"
"github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2"
"golang.org/x/exp/slices"
- "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
+ appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/build"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
- "gitea.plemya-x.ru/Plemya-x/ALR/pkg/repos"
)
func ListCmd() *cli.Command {
@@ -47,29 +47,26 @@ func ListCmd() *cli.Command {
},
},
Action: func(c *cli.Context) error {
+ if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
+ return err
+ }
+
ctx := c.Context
- cfg := config.New()
- err := cfg.Load()
- if err != nil {
- slog.Error(gotext.Get("Error loading config"), "err", err)
- os.Exit(1)
- }
- db := database.New(cfg)
- err = db.Init(ctx)
+ deps, err := appbuilder.
+ New(ctx).
+ WithConfig().
+ WithDB().
+ // autoPull only
+ WithRepos().
+ Build()
if err != nil {
- slog.Error(gotext.Get("Error initialization database"), "err", err)
- os.Exit(1)
+ return err
}
- rs := repos.New(cfg, db)
+ defer deps.Defer()
- if cfg.AutoPull() {
- err = rs.Pull(ctx, cfg.Repos())
- if err != nil {
- slog.Error(gotext.Get("Error pulling repositories"), "err", err)
- os.Exit(1)
- }
- }
+ cfg := deps.Cfg
+ db := deps.DB
where := "true"
args := []any(nil)
@@ -80,8 +77,7 @@ func ListCmd() *cli.Command {
result, err := db.GetPkgs(ctx, where, args...)
if err != nil {
- slog.Error(gotext.Get("Error getting packages"), "err", err)
- os.Exit(1)
+ return cliutils.FormatCliExit(gotext.Get("Error getting packages"), err)
}
defer result.Close()
@@ -89,14 +85,13 @@ func ListCmd() *cli.Command {
if c.Bool("installed") {
mgr := manager.Detect()
if mgr == nil {
- slog.Error(gotext.Get("Unable to detect a supported package manager on the system"))
- os.Exit(1)
+ return cli.Exit(gotext.Get("Unable to detect a supported package manager on the system"), 1)
}
- installed, err := mgr.ListInstalled(&manager.Opts{AsRoot: false})
+ installed, err := mgr.ListInstalled(&manager.Opts{})
if err != nil {
slog.Error(gotext.Get("Error listing installed packages"), "err", err)
- os.Exit(1)
+ return cli.Exit(err, 1)
}
for pkgName, version := range installed {
@@ -113,7 +108,7 @@ func ListCmd() *cli.Command {
var pkg database.Package
err := result.StructScan(&pkg)
if err != nil {
- return err
+ return cli.Exit(err, 1)
}
if slices.Contains(cfg.IgnorePkgUpdates(), pkg.Name) {
@@ -133,11 +128,6 @@ func ListCmd() *cli.Command {
fmt.Printf("%s/%s %s\n", pkg.Repository, pkg.Name, version)
}
- if err != nil {
- slog.Error(gotext.Get("Error iterating over packages"), "err", err)
- os.Exit(1)
- }
-
return nil
},
}
diff --git a/main.go b/main.go
index aa3aa4c..7505f30 100644
--- a/main.go
+++ b/main.go
@@ -82,29 +82,22 @@ func GetApp() *cli.App {
HelperCmd(),
VersionCmd(),
SearchCmd(),
+ // Internal commands
+ InternalBuildCmd(),
+ InternalInstallCmd(),
+ InternalMountCmd(),
},
Before: func(c *cli.Context) error {
- cfg := config.New()
- err := cfg.Load()
- if err != nil {
- slog.Error(gotext.Get("Error loading config"), "err", err)
- os.Exit(1)
- }
-
- cmd := c.Args().First()
- if cmd != "helper" && !cfg.AllowRunAsRoot() && os.Geteuid() == 0 {
- slog.Error(gotext.Get("Running ALR as root is forbidden as it may cause catastrophic damage to your system"))
- os.Exit(1)
- }
-
if trimmed := strings.TrimSpace(c.String("pm-args")); trimmed != "" {
args := strings.Split(trimmed, " ")
manager.Args = append(manager.Args, args...)
}
-
return nil
},
EnableBashCompletion: true,
+ ExitErrHandler: func(cCtx *cli.Context, err error) {
+ cliutils.HandleExitCoder(err)
+ },
}
}
@@ -142,8 +135,6 @@ func main() {
os.Exit(1)
}
setLogLevel(cfg.LogLevel())
- // Set the root command to the one set in the ALR config
- manager.DefaultRootCmd = cfg.RootCmd()
ctx, cancel := signal.NotifyContext(ctx, syscall.SIGINT, syscall.SIGTERM)
defer cancel()
diff --git a/pkg/build/build.go b/pkg/build/build.go
index 7b5c5ff..31c9e93 100644
--- a/pkg/build/build.go
+++ b/pkg/build/build.go
@@ -22,39 +22,162 @@ package build
import (
"bytes"
"context"
- "encoding/hex"
+ "encoding/gob"
"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"
+ "mvdan.cc/sh/v3/syntax/typedjson"
"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/db"
- "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"
- finddeps "gitea.plemya-x.ru/Plemya-x/ALR/pkg/build/find_deps"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
- "gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
)
+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 ScriptFile struct {
+ File *syntax.File
+ Path string
+}
+
+func (s *ScriptFile) GobEncode() ([]byte, error) {
+ var buf bytes.Buffer
+ enc := gob.NewEncoder(&buf)
+ if err := enc.Encode(s.Path); err != nil {
+ return nil, err
+ }
+ var fileBuf bytes.Buffer
+ if err := typedjson.Encode(&fileBuf, s.File); err != nil {
+ return nil, err
+ }
+ fileData := fileBuf.Bytes()
+ if err := enc.Encode(fileData); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func (s *ScriptFile) GobDecode(data []byte) error {
+ buf := bytes.NewBuffer(data)
+ dec := gob.NewDecoder(buf)
+ if err := dec.Decode(&s.Path); err != nil {
+ return err
+ }
+ var fileData []byte
+ if err := dec.Decode(&fileData); err != nil {
+ return err
+ }
+ fileReader := bytes.NewReader(fileData)
+ file, err := typedjson.Decode(fileReader)
+ if err != nil {
+ return err
+ }
+ s.File = file.(*syntax.File)
+ return nil
+}
+
+type BuildResult struct {
+ PackagePaths []string
+ PackageNames []string
+}
+
type PackageFinder interface {
FindPkgs(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error)
}
@@ -64,75 +187,192 @@ type Config interface {
PagerStyle() string
}
-type Builder struct {
- ctx context.Context
- opts types.BuildOpts
- info *distro.OSRelease
- repos PackageFinder
- config Config
+type FunctionsOutput struct {
+ Contents *[]string
}
+// EXECUTORS
+
+type ScriptResolverExecutor interface {
+ ResolveScript(ctx context.Context, pkg *db.Package) *ScriptInfo
+}
+
+type ScriptExecutor interface {
+ ReadScript(ctx context.Context, scriptPath string) (*ScriptFile, error)
+ ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *ScriptFile) (string, []*types.BuildVars, error)
+ PrepareDirs(
+ ctx context.Context,
+ input *BuildInput,
+ basePkg string,
+ ) error
+ ExecuteSecondPass(
+ ctx context.Context,
+ input *BuildInput,
+ sf *ScriptFile,
+ varsOfPackages []*types.BuildVars,
+ repoDeps []string,
+ builtNames []string,
+ basePkg string,
+ ) (*SecondPassResult, error)
+}
+
+type CacheExecutor interface {
+ CheckForBuiltPackage(ctx context.Context, input *BuildInput, vars *types.BuildVars) (string, bool, error)
+}
+
+type ScriptViewerExecutor interface {
+ ViewScript(ctx context.Context, input *BuildInput, sf *ScriptFile, basePkg string) error
+}
+
+type CheckerExecutor interface {
+ PerformChecks(
+ ctx context.Context,
+ input *BuildInput,
+ vars *types.BuildVars,
+ ) (bool, error)
+}
+
+type InstallerExecutor interface {
+ InstallLocal(paths []string) error
+ Install(pkgs []string) 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(
- ctx context.Context,
- opts types.BuildOpts,
- repos PackageFinder,
- info *distro.OSRelease,
- config Config,
+ scriptResolver ScriptResolverExecutor,
+ scriptExecutor ScriptExecutor,
+ cacheExecutor CacheExecutor,
+ scriptViewerExecutor ScriptViewerExecutor,
+ checkerExecutor CheckerExecutor,
+ installerExecutor InstallerExecutor,
+ sourceExecutor SourceDownloaderExecutor,
) *Builder {
return &Builder{
- ctx: ctx,
- opts: opts,
- info: info,
- repos: repos,
- config: config,
+ scriptResolver: scriptResolver,
+ scriptExecutor: scriptExecutor,
+ cacheExecutor: cacheExecutor,
+ scriptViewerExecutor: scriptViewerExecutor,
+ checkerExecutor: checkerExecutor,
+ installerExecutor: installerExecutor,
+ sourceExecutor: sourceExecutor,
}
}
-func (b *Builder) UpdateOptsFromPkg(pkg *db.Package, packages []string) {
- repodir := b.config.GetPaths().RepoDir
- b.opts.Repository = pkg.Repository
- if pkg.BasePkgName != "" {
- b.opts.Script = filepath.Join(repodir, pkg.Repository, pkg.BasePkgName, "alr.sh")
- b.opts.Packages = packages
- } else {
- b.opts.Script = filepath.Join(repodir, pkg.Repository, pkg.Name, "alr.sh")
- }
+type Builder struct {
+ scriptResolver ScriptResolverExecutor
+ scriptExecutor ScriptExecutor
+ cacheExecutor CacheExecutor
+ scriptViewerExecutor ScriptViewerExecutor
+ checkerExecutor CheckerExecutor
+ installerExecutor InstallerExecutor
+ sourceExecutor SourceDownloaderExecutor
+ repos PackageFinder
+ // mgr manager.Manager
}
-func (b *Builder) BuildPackage(ctx context.Context) ([]string, []string, error) {
- fl, err := readScript(b.opts.Script)
+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 *db.Package
+ Packages []string
+}
+
+type BuildPackageFromScriptArgs struct {
+ BuildArgs
+ Script string
+ Packages []string
+}
+
+func (b *Builder) BuildPackageFromDb(
+ ctx context.Context,
+ args *BuildPackageFromDbArgs,
+) (*BuildResult, 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,
+) (*BuildResult, 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,
+) (*BuildResult, error) {
+ scriptPath := input.script
+
+ slog.Debug("ReadScript")
+ sf, err := b.scriptExecutor.ReadScript(ctx, scriptPath)
if err != nil {
- return nil, nil, err
+ return nil, err
}
- // Первый проход предназначен для получения значений переменных и выполняется
- // до отображения скрипта, чтобы предотвратить выполнение вредоносного кода.
- basePkg, varsOfPackages, err := b.executeFirstPass(fl)
+ slog.Debug("ExecuteFirstPass")
+ basePkg, varsOfPackages, err := b.scriptExecutor.ExecuteFirstPass(ctx, input, sf)
if err != nil {
- return nil, nil, err
- }
-
- dirs, err := b.getDirs(basePkg)
- if err != nil {
- return nil, nil, err
+ return nil, err
}
builtPaths := make([]string, 0)
- // Если флаг opts.Clean не установлен, и пакет уже собран,
- // возвращаем его, а не собираем заново.
- if !b.opts.Clean {
+ if !input.opts.Clean {
var remainingVars []*types.BuildVars
for _, vars := range varsOfPackages {
- builtPkgPath, ok, err := b.checkForBuiltPackage(
- vars,
- getPkgFormat(b.opts.Manager),
- dirs.BaseDir,
- )
+ builtPkgPath, ok, err := b.cacheExecutor.CheckForBuiltPackage(ctx, input, vars)
if err != nil {
- return nil, nil, err
+ return nil, err
}
-
if ok {
builtPaths = append(builtPaths, builtPkgPath)
} else {
@@ -141,52 +381,26 @@ func (b *Builder) BuildPackage(ctx context.Context) ([]string, []string, error)
}
if len(remainingVars) == 0 {
- return builtPaths, nil, nil
+ return &BuildResult{builtPaths, nil}, nil
}
}
- // Спрашиваем у пользователя, хочет ли он увидеть скрипт сборки.
- err = cliutils.PromptViewScript(
- ctx,
- b.opts.Script,
- basePkg,
- b.config.PagerStyle(),
- b.opts.Interactive,
- )
+ slog.Debug("ViewScript")
+ err = b.scriptViewerExecutor.ViewScript(ctx, input, sf, basePkg)
if err != nil {
- slog.Error(gotext.Get("Failed to prompt user to view build script"), "err", err)
- os.Exit(1)
+ return nil, err
}
slog.Info(gotext.Get("Building package"), "name", basePkg)
- // Второй проход будет использоваться для выполнения реального кода,
- // поэтому он не ограничен. Скрипт уже был показан
- // пользователю к этому моменту, так что это должно быть безопасно.
- 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
- }
-
for _, vars := range varsOfPackages {
- cont, err := b.performChecks(ctx, vars, installed) // Выполняем различные проверки
+ cont, err := b.checkerExecutor.PerformChecks(ctx, input, vars)
if err != nil {
- return nil, nil, err
- } else if !cont {
- os.Exit(1) // Если проверки не пройдены, выходим из программы
+ return nil, err
+ }
+ if !cont {
+ return nil, errors.New("exit...")
}
- }
-
- // Подготавливаем директории для сборки
- err = prepareDirs(dirs)
- if err != nil {
- return nil, nil, err
}
buildDepends := []string{}
@@ -207,319 +421,124 @@ func (b *Builder) BuildPackage(ctx context.Context) ([]string, []string, error)
if len(sources) != len(checksums) {
slog.Error(gotext.Get("The checksums array must be the same length as sources"))
- os.Exit(1)
+ return nil, errors.New("exit...")
}
sources, checksums = removeDuplicatesSources(sources, checksums)
- mergedVars := types.BuildVars{
- BuildVarsPre: types.BuildVarsPre{
+ slog.Debug("installBuildDeps")
+ 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
+ }
+
+ slog.Debug("BuildALRDeps")
+ _, builtNames, repoDeps, err := b.BuildALRDeps(ctx, input, depends)
+ if err != nil {
+ return nil, err
+ }
+
+ slog.Debug("PrepareDirs")
+ err = b.scriptExecutor.PrepareDirs(ctx, input, basePkg)
+ if err != nil {
+ return nil, err
+ }
+
+ // builtPaths = append(builtPaths, newBuildPaths...)
+
+ slog.Info(gotext.Get("Downloading sources"))
+ slog.Debug("DownloadSources")
+ err = b.sourceExecutor.DownloadSources(
+ ctx,
+ input,
+ basePkg,
+ SourcesInput{
Sources: sources,
Checksums: checksums,
},
- }
-
- buildDeps, err := b.installBuildDeps(ctx, buildDepends) // Устанавливаем зависимости для сборки
- if err != nil {
- return nil, nil, err
- }
-
- err = b.installOptDeps(ctx, optDepends) // Устанавливаем опциональные зависимости
- if err != nil {
- return nil, nil, err
- }
-
- newBuildPaths, builtNames, repoDeps, err := b.buildALRDeps(ctx, depends) // Собираем зависимости
- if err != nil {
- return nil, nil, err
- }
-
- builtPaths = append(builtPaths, newBuildPaths...)
-
- slog.Info(gotext.Get("Downloading sources")) // Записываем в лог загрузку источников
-
- err = b.getSources(ctx, dirs, &mergedVars) // Загружаем исходники
- if err != nil {
- return nil, nil, err
- }
-
- err = b.executeFunctions(ctx, dec, dirs) // Выполняем специальные функции
- if err != nil {
- return nil, nil, err
- }
-
- for _, vars := range varsOfPackages {
- packageName := ""
- if vars.Base != "" {
- packageName = vars.Name
- }
- funcOut, err := b.executePackageFunctions(ctx, dec, dirs, packageName)
- if err != nil {
- return nil, nil, err
- }
-
- slog.Info(gotext.Get("Building package metadata"), "name", basePkg)
-
- pkgFormat := getPkgFormat(b.opts.Manager) // Получаем формат пакета
-
- pkgInfo, err := b.buildPkgMetadata(ctx, vars, dirs, pkgFormat, 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
- }
-
- // Добавляем путь и имя только что собранного пакета в
- // соответствующие срезы
- builtPaths = append(builtPaths, pkgPath)
- builtNames = append(builtNames, vars.Name)
- }
-
- err = b.removeBuildDeps(ctx, buildDeps) // Удаляем зависимости для сборки
- if err != nil {
- return nil, nil, err
- }
-
- // Удаляем дубликаты из pkgPaths и pkgNames.
- // Дубликаты могут появиться, если несколько зависимостей
- // зависят от одних и тех же пакетов.
- pkgPaths := removeDuplicates(builtPaths)
- pkgNames := removeDuplicates(builtNames)
-
- return pkgPaths, pkgNames, nil // Возвращаем пути и имена пакетов
-}
-
-// Функция executeFirstPass выполняет парсированный скрипт в ограниченной среде,
-// чтобы извлечь переменные сборки без выполнения реального кода.
-func (b *Builder) executeFirstPass(
- fl *syntax.File,
-) (string, []*types.BuildVars, error) {
- varsOfPackages := []*types.BuildVars{}
-
- 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.ReadDirHandler2(handlers.RestrictedReadDir(scriptDir)), // Ограничиваем чтение директорий
- interp.StatHandler(handlers.RestrictedStat(scriptDir)), // Ограничиваем доступ к статистике файлов
- interp.OpenHandler(handlers.RestrictedOpen(scriptDir)), // Ограничиваем открытие файлов
)
if err != nil {
- return "", nil, err
+ return nil, err
}
- err = runner.Run(b.ctx, fl) // Запускаем скрипт
+ slog.Debug("ExecuteSecondPass")
+ res, err := b.scriptExecutor.ExecuteSecondPass(
+ ctx,
+ input,
+ sf,
+ varsOfPackages,
+ repoDeps,
+ builtNames,
+ basePkg,
+ )
if err != nil {
- return "", nil, err
+ return nil, err
}
- dec := decoder.New(b.info, runner) // Создаём новый декодер
+ pkgPaths := removeDuplicates(res.BuiltPaths)
+ pkgNames := removeDuplicates(res.BuiltNames)
- 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(b.opts.Packages) == 0 {
- return "", nil, errors.New("script has multiple packages but package is not specified")
- }
-
- for _, pkgName := range b.opts.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(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 = pkgName
- vars.Base = pkgs.BasePkgName
-
- varsOfPackages = append(varsOfPackages, &vars)
- }
-
- return pkgs.BasePkgName, varsOfPackages, nil // Возвращаем переменные сборки
-}
-
-// Функция getDirs возвращает соответствующие директории для скрипта
-func (b *Builder) getDirs(basePkg string) (types.Directories, error) {
- scriptPath, err := filepath.Abs(b.opts.Script)
- if err != nil {
- return types.Directories{}, err
- }
-
- baseDir := filepath.Join(b.config.GetPaths().PkgsDir, basePkg) // Определяем базовую директорию
- return types.Directories{
- BaseDir: baseDir,
- SrcDir: filepath.Join(baseDir, "src"),
- PkgDir: filepath.Join(baseDir, "pkg"),
- ScriptDir: filepath.Dir(scriptPath),
+ return &BuildResult{
+ PackagePaths: pkgPaths,
+ PackageNames: pkgNames,
}, nil
}
-// Функция executeSecondPass выполняет скрипт сборки второй раз без каких-либо ограничений. Возвращается декодер,
-// который может быть использован для получения функций и переменных из скрипта.
-func (b *Builder) executeSecondPass(
+type InstallPkgsArgs struct {
+ BuildArgs
+ AlrPkgs []db.Package
+ NativePkgs []string
+}
+
+func (b *Builder) InstallALRPackages(
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.ExecHandlers(func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
- return 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(
+ input interface {
+ OsInfoProvider
+ BuildOptsProvider
+ PkgFormatProvider
+ },
+ alrPkgs []db.Package,
+) error {
+ for _, pkg := range alrPkgs {
+ res, err := b.BuildPackageFromDb(
ctx,
- gotext.Get("Your system's CPU architecture doesn't match this package. Do you want to build anyway?"),
- b.opts.Interactive,
- true,
+ &BuildPackageFromDbArgs{
+ Package: &pkg,
+ Packages: []string{},
+ BuildArgs: BuildArgs{
+ Opts: input.BuildOpts(),
+ Info: input.OSRelease(),
+ PkgFormat_: input.PkgFormat(),
+ },
+ },
)
if err != nil {
- return false, err
+ return 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, buildDepends []string) ([]string, error) {
- var buildDeps []string
- if len(buildDepends) > 0 {
- deps, err := removeAlreadyInstalled(b.opts, buildDepends)
+ err = b.installerExecutor.InstallLocal(res.PackagePaths)
if err != nil {
- return nil, err
+ return err
}
-
- found, notFound, err := b.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)
- b.InstallPkgs(ctx, flattened, notFound, b.opts) // Устанавливаем пакеты
}
- return buildDeps, nil
+
+ return nil
}
-func (b *Builder) getBuildersForPackages(pkgs []db.Package) []*Builder {
- type item struct {
- pkg *db.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,
- )
- }
-
- builders := []*Builder{}
-
- for basePkgName := range pkgsMap {
- pkg := pkgsMap[basePkgName].pkg
- builder := *b
- builder.UpdateOptsFromPkg(pkg, pkgsMap[basePkgName].packages)
- builders = append(builders, &builder)
- }
-
- return builders
-}
-
-func (b *Builder) buildALRDeps(ctx context.Context, depends []string) (builtPaths, builtNames, repoDeps []string, err error) {
+func (b *Builder) BuildALRDeps(
+ ctx context.Context,
+ input interface {
+ OsInfoProvider
+ BuildOptsProvider
+ PkgFormatProvider
+ },
+ depends []string,
+) (builtPaths, builtNames, repoDeps []string, err error) {
if len(depends) > 0 {
slog.Info(gotext.Get("Installing dependencies"))
@@ -530,19 +549,53 @@ func (b *Builder) buildALRDeps(ctx context.Context, depends []string) (builtPath
repoDeps = notFound
// Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез
- pkgs := cliutils.FlattenPkgs(ctx, found, "install", b.opts.Interactive)
- builders := b.getBuildersForPackages(pkgs)
- for _, builder := range builders {
- // Собираем зависимости
- pkgPaths, pkgNames, err := builder.BuildPackage(ctx)
+ pkgs := cliutils.FlattenPkgs(
+ ctx,
+ found,
+ "install",
+ input.BuildOpts().Interactive,
+ )
+ type item struct {
+ pkg *db.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, nil, err
}
- // Добавляем пути всех собранных пакетов в builtPaths
- builtPaths = append(builtPaths, pkgPaths...)
- // Добавляем пути всех собранных пакетов в builtPaths
- builtNames = append(builtNames, pkgNames...)
+ builtPaths = append(builtPaths, res.PackagePaths...)
+ builtNames = append(builtNames, res.PackageNames...)
}
}
@@ -554,207 +607,49 @@ func (b *Builder) buildALRDeps(ctx context.Context, depends []string) (builtPath
return builtPaths, builtNames, repoDeps, nil
}
-func (b *Builder) getSources(ctx context.Context, dirs types.Directories, bv *types.BuildVars) error {
- 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(
+func (i *Builder) installBuildDeps(
ctx context.Context,
- dec *decoder.Decoder,
- dirs types.Directories,
+ input interface {
+ OsInfoProvider
+ BuildOptsProvider
+ PkgFormatProvider
+ },
+ pkgs []string,
) 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)
+ if len(pkgs) > 0 {
+ deps, err := i.installerExecutor.RemoveAlreadyInstalled(pkgs)
+ if err != nil {
+ return err
}
- */
- prepare, ok := dec.GetFunc("prepare")
- if ok {
- slog.Info(gotext.Get("Executing prepare()"))
-
- err := prepare(ctx, interp.Dir(dirs.SrcDir))
+ err = i.InstallPkgs(ctx, input, deps) // Устанавливаем выбранные пакеты
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 (b *Builder) executePackageFunctions(
+func (i *Builder) installOptDeps(
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
-}
-
-func (b *Builder) installOptDeps(ctx context.Context, optDepends []string) error {
- optDeps, err := removeAlreadyInstalled(b.opts, optDepends)
+ input interface {
+ OsInfoProvider
+ BuildOptsProvider
+ PkgFormatProvider
+ },
+ pkgs []string,
+) error {
+ optDeps, err := i.installerExecutor.RemoveAlreadyInstalled(pkgs)
if err != nil {
return err
}
if len(optDeps) > 0 {
- optDeps, err := cliutils.ChooseOptDepends(ctx, optDeps, "install", b.opts.Interactive) // Пользователя просят выбрать опциональные зависимости
+ optDeps, err := cliutils.ChooseOptDepends(
+ ctx,
+ optDeps,
+ "install",
+ input.BuildOpts().Interactive,
+ ) // Пользователя просят выбрать опциональные зависимости
if err != nil {
return err
}
@@ -763,153 +658,41 @@ func (b *Builder) installOptDeps(ctx context.Context, optDepends []string) error
return nil
}
- found, notFound, err := b.repos.FindPkgs(ctx, optDeps) // Находим опциональные зависимости
+ err = i.InstallPkgs(ctx, input, optDeps) // Устанавливаем выбранные пакеты
if err != nil {
return err
}
-
- flattened := cliutils.FlattenPkgs(ctx, found, "install", b.opts.Interactive)
- b.InstallPkgs(ctx, flattened, notFound, b.opts) // Устанавливаем выбранные пакеты
}
return nil
}
-func (b *Builder) InstallPkgs(
+func (i *Builder) InstallPkgs(
ctx context.Context,
- alrPkgs []db.Package,
- nativePkgs []string,
- opts types.BuildOpts,
-) {
- if len(nativePkgs) > 0 {
- err := opts.Manager.Install(nil, nativePkgs...)
- // Если есть нативные пакеты, выполняем их установку
- if err != nil {
- slog.Error(gotext.Get("Error installing native packages"), "err", err)
- os.Exit(1)
- // Логируем и завершаем выполнение при ошибке
- }
- }
-
- b.InstallALRPackages(ctx, alrPkgs, opts)
- // Устанавливаем скрипты сборки через функцию InstallScripts
-}
-
-func (b *Builder) InstallALRPackages(ctx context.Context, pkgs []db.Package, opts types.BuildOpts) {
- builders := b.getBuildersForPackages(pkgs)
- for _, builder := range builders {
- builtPkgs, _, err := builder.BuildPackage(ctx)
- // Выполняем сборку пакета
- if err != nil {
- slog.Error(gotext.Get("Error building package"), "err", err)
- os.Exit(1)
- // Логируем и завершаем выполнение при ошибке сборки
- }
-
- err = opts.Manager.InstallLocal(nil, builtPkgs...)
- // Устанавливаем локально собранные пакеты
- if err != nil {
- slog.Error(gotext.Get("Error installing package"), "err", err)
- os.Exit(1)
- // Логируем и завершаем выполнение при ошибке установки
- }
- }
-}
-
-// Функция buildPkgMetadata создает метаданные для пакета, который будет собран.
-func (b *Builder) buildPkgMetadata(
- ctx context.Context,
- vars *types.BuildVars,
- dirs types.Directories,
- pkgFormat string,
- deps []string,
- preferedContents *[]string,
-) (*nfpm.Info, error) {
- pkgInfo := getBasePkgInfo(vars, b.info, &b.opts)
- 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,
- }
-
- 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)
+ input interface {
+ OsInfoProvider
+ BuildOptsProvider
+ PkgFormatProvider
+ },
+ pkgs []string,
+) error {
+ builtPaths, _, repoDeps, err := i.BuildALRDeps(ctx, input, pkgs)
if err != nil {
- return nil, err
+ return err
}
- pkgInfo.Overridables.Contents = contents
- if len(vars.AutoProv) == 1 && decoder.IsTruthy(vars.AutoProv[0]) {
- f := finddeps.New(b.info, pkgFormat)
- err = f.FindProvides(ctx, pkgInfo, dirs, vars.AutoProvSkipList)
+ if len(builtPaths) > 0 {
+ err = i.installerExecutor.InstallLocal(builtPaths)
if err != nil {
- return nil, err
+ return err
}
}
- if len(vars.AutoReq) == 1 && decoder.IsTruthy(vars.AutoReq[0]) {
- f := finddeps.New(b.info, pkgFormat)
- err = f.FindRequires(ctx, pkgInfo, dirs, vars.AutoReqSkipList)
+ if len(repoDeps) > 0 {
+ err = i.installerExecutor.Install(repoDeps)
if err != nil {
- return nil, err
+ return err
}
}
- return pkgInfo, nil
-}
-
-// Функция checkForBuiltPackage пытается обнаружить ранее собранный пакет и вернуть его путь
-// и true, если нашла. Если нет, возвратит "", false, nil.
-func (b *Builder) checkForBuiltPackage(
- vars *types.BuildVars,
- pkgFormat,
- baseDir string,
-) (string, bool, error) {
- filename, err := b.pkgFileName(vars, pkgFormat)
- if err != nil {
- return "", false, err
- }
-
- pkgPath := filepath.Join(baseDir, filename)
-
- _, err = os.Stat(pkgPath)
- if err != nil {
- return "", false, nil
- }
-
- return pkgPath, true, nil
-}
-
-// pkgFileName returns the filename of the package if it were to be built.
-// This is used to check if the package has already been built.
-func (b *Builder) pkgFileName(vars *types.BuildVars, pkgFormat string) (string, error) {
- pkgInfo := getBasePkgInfo(vars, b.info, &b.opts)
-
- packager, err := nfpm.Get(pkgFormat)
- if err != nil {
- return "", err
- }
-
- return packager.ConventionalFileName(pkgInfo), nil
+ return nil
}
diff --git a/pkg/build/build_internal_test.go b/pkg/build/build_internal_test.need-to-update
similarity index 99%
rename from pkg/build/build_internal_test.go
rename to pkg/build/build_internal_test.need-to-update
index 69138d6..55df7c4 100644
--- a/pkg/build/build_internal_test.go
+++ b/pkg/build/build_internal_test.need-to-update
@@ -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)
diff --git a/pkg/build/cache.go b/pkg/build/cache.go
new file mode 100644
index 0000000..6de0b7c
--- /dev/null
+++ b/pkg/build/cache.go
@@ -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 .
+
+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
+}
diff --git a/pkg/build/checker.go b/pkg/build/checker.go
new file mode 100644
index 0000000..827d274
--- /dev/null
+++ b/pkg/build/checker.go
@@ -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 .
+
+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
+}
diff --git a/pkg/build/dirs.go b/pkg/build/dirs.go
new file mode 100644
index 0000000..58038d8
--- /dev/null
+++ b/pkg/build/dirs.go
@@ -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 .
+
+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)
+}
diff --git a/pkg/build/installer.go b/pkg/build/installer.go
new file mode 100644
index 0000000..d4e86ad
--- /dev/null
+++ b/pkg/build/installer.go
@@ -0,0 +1,54 @@
+// 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 (
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
+)
+
+func NewInstaller(mgr manager.Manager) *Installer {
+ return &Installer{
+ mgr: mgr,
+ }
+}
+
+type Installer struct{ 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
+}
diff --git a/pkg/build/main_build.go b/pkg/build/main_build.go
new file mode 100644
index 0000000..aecd866
--- /dev/null
+++ b/pkg/build/main_build.go
@@ -0,0 +1,52 @@
+// 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 (
+ "gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
+)
+
+func NewMainBuilder(
+ cfg Config,
+ mgr manager.Manager,
+ repos PackageFinder,
+ scriptExecutor ScriptExecutor,
+ installerExecutor InstallerExecutor,
+) (*Builder, error) {
+ builder := &Builder{
+ scriptExecutor: scriptExecutor,
+ cacheExecutor: &Cache{
+ cfg,
+ },
+ scriptResolver: &ScriptResolver{
+ cfg,
+ },
+ scriptViewerExecutor: &ScriptViewer{
+ config: cfg,
+ },
+ checkerExecutor: &Checker{
+ mgr,
+ },
+ installerExecutor: installerExecutor,
+ sourceExecutor: &SourceDownloader{
+ cfg,
+ },
+ repos: repos,
+ }
+
+ return builder, nil
+}
diff --git a/pkg/build/safe_installer.go b/pkg/build/safe_installer.go
new file mode 100644
index 0000000..2d143e4
--- /dev/null
+++ b/pkg/build/safe_installer.go
@@ -0,0 +1,160 @@
+// 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 (
+ "fmt"
+ "log/slog"
+ "net/rpc"
+ "os"
+ "os/exec"
+ "sync"
+ "syscall"
+
+ "github.com/hashicorp/go-plugin"
+
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
+)
+
+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 {
+ 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) {
+ var val []string
+ err := r.client.Call("Plugin.RemoveAlreadyInstalled", paths, &val)
+ return val, 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, func(), error) {
+ var err error
+
+ executable, err := os.Executable()
+ if err != nil {
+ return nil, nil, err
+ }
+ cmd := exec.Command(executable, "_internal-installer")
+ cmd.Env = []string{
+ "HOME=/var/cache/alr",
+ "LOGNAME=alr",
+ "USER=alr",
+ "PATH=/usr/bin:/bin:/usr/local/bin",
+ "ALR_LOG_LEVEL=DEBUG",
+ }
+
+ /*
+ uid, gid, err := utils.GetUidGidAlrUser()
+ if err != nil {
+ return nil, nil, err
+ }
+
+
+ cmd.SysProcAttr = &syscall.SysProcAttr{
+ Credential: &syscall.Credential{
+ Uid: uint32(uid),
+ Gid: uint32(gid),
+ },
+ }
+ */
+
+ slog.Debug("safe installer setup", "uid", syscall.Getuid(), "gid", syscall.Getgid())
+
+ 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 {
+ return nil, nil, err
+ }
+
+ var cleanupOnce sync.Once
+ cleanup := func() {
+ cleanupOnce.Do(func() {
+ client.Kill()
+ })
+ }
+
+ defer func() {
+ if err != nil {
+ slog.Debug("close installer")
+ cleanup()
+ }
+ }()
+
+ raw, err := rpcClient.Dispense("installer")
+ if err != nil {
+ return nil, nil, err
+ }
+
+ executor, ok := raw.(InstallerExecutor)
+ if !ok {
+ err = fmt.Errorf("dispensed object is not a ScriptExecutor (got %T)", raw)
+ return nil, nil, err
+ }
+
+ return executor, cleanup, nil
+}
diff --git a/pkg/build/safe_script_executor.go b/pkg/build/safe_script_executor.go
new file mode 100644
index 0000000..a9b1cb7
--- /dev/null
+++ b/pkg/build/safe_script_executor.go
@@ -0,0 +1,291 @@
+// 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 (
+ "context"
+ "fmt"
+ "log/slog"
+ "net/rpc"
+ "os"
+ "os/exec"
+ "sync"
+
+ "github.com/hashicorp/go-plugin"
+
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
+)
+
+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, func(), error) {
+ var err error
+
+ executable, err := os.Executable()
+ if err != nil {
+ return nil, 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",
+ "ALR_LOG_LEVEL=DEBUG",
+ }
+ /*
+ uid, gid, err := utils.GetUidGidAlrUser()
+ if err != nil {
+ return nil, 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",
+ },
+ })
+ rpcClient, err := client.Client()
+ if err != nil {
+ return nil, nil, err
+ }
+
+ var cleanupOnce sync.Once
+ cleanup := func() {
+ cleanupOnce.Do(func() {
+ client.Kill()
+ })
+ }
+
+ defer func() {
+ if err != nil {
+ slog.Debug("close script-executor")
+ cleanup()
+ }
+ }()
+
+ raw, err := rpcClient.Dispense("script-executor")
+ if err != nil {
+ return nil, nil, err
+ }
+
+ executor, ok := raw.(ScriptExecutor)
+ if !ok {
+ err = fmt.Errorf("dispensed object is not a ScriptExecutor (got %T)", raw)
+ return nil, nil, err
+ }
+
+ return executor, cleanup, nil
+}
diff --git a/pkg/build/script_executor.go b/pkg/build/script_executor.go
new file mode 100644
index 0000000..cb4b1e5
--- /dev/null
+++ b/pkg/build/script_executor.go
@@ -0,0 +1,435 @@
+// 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
+}
diff --git a/pkg/build/script_resolver.go b/pkg/build/script_resolver.go
new file mode 100644
index 0000000..363bbba
--- /dev/null
+++ b/pkg/build/script_resolver.go
@@ -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 .
+
+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,
+ }
+}
diff --git a/pkg/build/script_view.go b/pkg/build/script_view.go
new file mode 100644
index 0000000..9347ba2
--- /dev/null
+++ b/pkg/build/script_view.go
@@ -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 .
+
+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,
+ )
+}
diff --git a/pkg/build/source_downloader.go b/pkg/build/source_downloader.go
new file mode 100644
index 0000000..715557a
--- /dev/null
+++ b/pkg/build/source_downloader.go
@@ -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 .
+
+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
+}
diff --git a/pkg/build/utils.go b/pkg/build/utils.go
index 33656c6..2d73ee0 100644
--- a/pkg/build/utils.go
+++ b/pkg/build/utils.go
@@ -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[^+]+)\+alr-(?P.+)$`)
-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 {
diff --git a/pkg/distro/osrelease.go b/pkg/distro/osrelease.go
index 8320f94..fc94a39 100644
--- a/pkg/distro/osrelease.go
+++ b/pkg/distro/osrelease.go
@@ -82,6 +82,7 @@ func ParseOSRelease(ctx context.Context) (*OSRelease, error) {
interp.ReadDirHandler2(handlers.NopReadDir),
interp.StatHandler(handlers.NopStat),
interp.Env(expand.ListEnviron()),
+ interp.Dir("/"),
)
if err != nil {
return nil, err
diff --git a/pkg/manager/apk.go b/pkg/manager/apk.go
index aaf0d11..136b76c 100644
--- a/pkg/manager/apk.go
+++ b/pkg/manager/apk.go
@@ -28,7 +28,15 @@ import (
// APK represents the APK package manager
type APK struct {
- rootCmd string
+ CommonPackageManager
+}
+
+func NewAPK() *APK {
+ return &APK{
+ CommonPackageManager: CommonPackageManager{
+ noConfirmArg: "-i",
+ },
+ }
}
func (*APK) Exists() bool {
@@ -44,10 +52,6 @@ func (*APK) Format() string {
return "apk"
}
-func (a *APK) SetRootCmd(s string) {
- a.rootCmd = s
-}
-
func (a *APK) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apk", "update")
@@ -163,20 +167,3 @@ func (a *APK) IsInstalled(pkg string) (bool, error) {
}
return true, nil
}
-
-func (a *APK) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
- var cmd *exec.Cmd
- if opts.AsRoot {
- cmd = exec.Command(getRootCmd(a.rootCmd), mgrCmd)
- cmd.Args = append(cmd.Args, opts.Args...)
- cmd.Args = append(cmd.Args, args...)
- } else {
- cmd = exec.Command(mgrCmd, args...)
- }
-
- if !opts.NoConfirm {
- cmd.Args = append(cmd.Args, "-i")
- }
-
- return cmd
-}
diff --git a/pkg/manager/apt.go b/pkg/manager/apt.go
index dda9e8d..2a55926 100644
--- a/pkg/manager/apt.go
+++ b/pkg/manager/apt.go
@@ -28,7 +28,15 @@ import (
// APT represents the APT package manager
type APT struct {
- rootCmd string
+ CommonPackageManager
+}
+
+func NewAPT() *APT {
+ return &APT{
+ CommonPackageManager: CommonPackageManager{
+ noConfirmArg: "-y",
+ },
+ }
}
func (*APT) Exists() bool {
@@ -44,10 +52,6 @@ func (*APT) Format() string {
return "deb"
}
-func (a *APT) SetRootCmd(s string) {
- a.rootCmd = s
-}
-
func (a *APT) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt", "update")
@@ -149,20 +153,3 @@ func (a *APT) IsInstalled(pkg string) (bool, error) {
}
return true, nil
}
-
-func (a *APT) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
- var cmd *exec.Cmd
- if opts.AsRoot {
- cmd = exec.Command(getRootCmd(a.rootCmd), mgrCmd)
- cmd.Args = append(cmd.Args, opts.Args...)
- cmd.Args = append(cmd.Args, args...)
- } else {
- cmd = exec.Command(mgrCmd, args...)
- }
-
- if opts.NoConfirm {
- cmd.Args = append(cmd.Args, "-y")
- }
-
- return cmd
-}
diff --git a/pkg/manager/apt_rpm.go b/pkg/manager/apt_rpm.go
index c3ce1a2..ffba855 100644
--- a/pkg/manager/apt_rpm.go
+++ b/pkg/manager/apt_rpm.go
@@ -24,18 +24,16 @@ import (
// APTRpm represents the APT-RPM package manager
type APTRpm struct {
+ CommonPackageManager
CommonRPM
- rootCmd string
}
-func (*APTRpm) Exists() bool {
- cmd := exec.Command("apt-config", "dump")
- output, err := cmd.Output()
- if err != nil {
- return false
+func NewAPTRpm() *APTRpm {
+ return &APTRpm{
+ CommonPackageManager: CommonPackageManager{
+ noConfirmArg: "-y",
+ },
}
-
- return strings.Contains(string(output), "RPM")
}
func (*APTRpm) Name() string {
@@ -46,8 +44,14 @@ func (*APTRpm) Format() string {
return "rpm"
}
-func (a *APTRpm) SetRootCmd(s string) {
- a.rootCmd = s
+func (*APTRpm) Exists() bool {
+ cmd := exec.Command("apt-config", "dump")
+ output, err := cmd.Output()
+ if err != nil {
+ return false
+ }
+
+ return strings.Contains(string(output), "RPM")
}
func (a *APTRpm) Sync(opts *Opts) error {
@@ -66,6 +70,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)
@@ -105,20 +110,3 @@ func (a *APTRpm) UpgradeAll(opts *Opts) error {
}
return nil
}
-
-func (a *APTRpm) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
- var cmd *exec.Cmd
- if opts.AsRoot {
- cmd = exec.Command(getRootCmd(a.rootCmd), mgrCmd)
- cmd.Args = append(cmd.Args, opts.Args...)
- cmd.Args = append(cmd.Args, args...)
- } else {
- cmd = exec.Command(mgrCmd, args...)
- }
-
- if opts.NoConfirm {
- cmd.Args = append(cmd.Args, "-y")
- }
-
- return cmd
-}
diff --git a/pkg/manager/common.go b/pkg/manager/common.go
new file mode 100644
index 0000000..c71135d
--- /dev/null
+++ b/pkg/manager/common.go
@@ -0,0 +1,35 @@
+// 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 manager
+
+import "os/exec"
+
+type CommonPackageManager struct {
+ noConfirmArg string
+}
+
+func (m *CommonPackageManager) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
+ cmd := exec.Command(mgrCmd)
+ cmd.Args = append(cmd.Args, opts.Args...)
+ cmd.Args = append(cmd.Args, args...)
+
+ if opts.NoConfirm {
+ cmd.Args = append(cmd.Args, m.noConfirmArg)
+ }
+
+ return cmd
+}
diff --git a/pkg/manager/dnf.go b/pkg/manager/dnf.go
index 079e967..3bbe29e 100644
--- a/pkg/manager/dnf.go
+++ b/pkg/manager/dnf.go
@@ -1,20 +1,21 @@
-/*
- * ALR - Any Linux Repository
- * ALR - Любой Linux Репозиторий
- * Copyright (C) 2024 Евгений Храмов
- *
- * This program является свободным: вы можете распространять его и/или изменять
- * на условиях GNU General Public License, опубликованной Free Software Foundation,
- * либо версии 3 лицензии, либо (по вашему выбору) любой более поздней версии.
- *
- * Это программное обеспечение распространяется в надежде, что оно будет полезным,
- * но БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ; без подразумеваемой гарантии
- * КОММЕРЧЕСКОЙ ПРИГОДНОСТИ или ПРИГОДНОСТИ ДЛЯ ОПРЕДЕЛЕННОЙ ЦЕЛИ.
- * Подробности см. в GNU General Public License.
- *
- * Вы должны были получить копию GNU General Public License
- * вместе с этой программой. Если нет, см. .
- */
+// 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 .
package manager
@@ -23,33 +24,32 @@ import (
"os/exec"
)
-// DNF представляет менеджер пакетов DNF
type DNF struct {
+ CommonPackageManager
CommonRPM
- rootCmd string // rootCmd хранит команду, используемую для выполнения команд с правами root
}
-// Exists проверяет, доступен ли DNF в системе, возвращает true если да
+func NewDNF() *DNF {
+ return &DNF{
+ CommonPackageManager: CommonPackageManager{
+ noConfirmArg: "-y",
+ },
+ }
+}
+
func (*DNF) Exists() bool {
_, err := exec.LookPath("dnf")
return err == nil
}
-// Name возвращает имя менеджера пакетов, в данном случае "dnf"
func (*DNF) Name() string {
return "dnf"
}
-// Format возвращает формат пакетов "rpm", используемый DNF
func (*DNF) Format() string {
return "rpm"
}
-// SetRootCmd устанавливает команду, используемую для выполнения операций с правами root
-func (d *DNF) SetRootCmd(s string) {
- d.rootCmd = s
-}
-
// Sync выполняет upgrade всех установленных пакетов, обновляя их до более новых версий
func (d *DNF) Sync(opts *Opts) error {
opts = ensureOpts(opts) // Гарантирует, что opts не равен nil и содержит допустимые значения
@@ -118,21 +118,3 @@ func (d *DNF) UpgradeAll(opts *Opts) error {
}
return nil
}
-
-// getCmd создает и возвращает команду exec.Cmd для менеджера пакетов DNF
-func (d *DNF) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
- var cmd *exec.Cmd
- if opts.AsRoot {
- cmd = exec.Command(getRootCmd(d.rootCmd), mgrCmd)
- cmd.Args = append(cmd.Args, opts.Args...)
- cmd.Args = append(cmd.Args, args...)
- } else {
- cmd = exec.Command(mgrCmd, args...)
- }
-
- if opts.NoConfirm {
- cmd.Args = append(cmd.Args, "-y") // Добавляет параметр автоматического подтверждения (-y)
- }
-
- return cmd
-}
diff --git a/pkg/manager/managers.go b/pkg/manager/managers.go
index 0219bd3..a38c355 100644
--- a/pkg/manager/managers.go
+++ b/pkg/manager/managers.go
@@ -27,27 +27,22 @@ import (
var Args []string
type Opts struct {
- AsRoot bool
NoConfirm bool
Args []string
}
var DefaultOpts = &Opts{
- AsRoot: true,
NoConfirm: false,
}
-// DefaultRootCmd is the command used for privilege elevation by default
-var DefaultRootCmd = "sudo"
-
var managers = []Manager{
- &Pacman{},
- &APT{},
- &DNF{},
- &YUM{},
- &APK{},
- &Zypper{},
- &APTRpm{},
+ NewPacman(),
+ NewAPT(),
+ NewDNF(),
+ NewYUM(),
+ NewAPK(),
+ NewZypper(),
+ NewAPTRpm(),
}
// Register registers a new package manager
@@ -64,8 +59,7 @@ type Manager interface {
Format() string
// Returns true if the package manager exists on the system.
Exists() bool
- // Sets the command used to elevate privileges. Defaults to DefaultRootCmd.
- SetRootCmd(string)
+
// Sync fetches repositories without installing anything
Sync(*Opts) error
// Install installs packages
@@ -104,18 +98,10 @@ func Get(name string) Manager {
return nil
}
-// getRootCmd returns rootCmd if it's not empty, otherwise returns DefaultRootCmd
-func getRootCmd(rootCmd string) string {
- if rootCmd != "" {
- return rootCmd
- }
- return DefaultRootCmd
-}
-
func setCmdEnv(cmd *exec.Cmd) {
cmd.Env = os.Environ()
cmd.Stdin = os.Stdin
- cmd.Stdout = os.Stdout
+ cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
}
diff --git a/pkg/manager/pacman.go b/pkg/manager/pacman.go
index 825bff4..1fe96df 100644
--- a/pkg/manager/pacman.go
+++ b/pkg/manager/pacman.go
@@ -28,7 +28,15 @@ import (
// Pacman represents the Pacman package manager
type Pacman struct {
- rootCmd string
+ CommonPackageManager
+}
+
+func NewPacman() *Pacman {
+ return &Pacman{
+ CommonPackageManager: CommonPackageManager{
+ noConfirmArg: "--noconfirm",
+ },
+ }
}
func (*Pacman) Exists() bool {
@@ -44,10 +52,6 @@ func (*Pacman) Format() string {
return "archlinux"
}
-func (p *Pacman) SetRootCmd(s string) {
- p.rootCmd = s
-}
-
func (p *Pacman) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := p.getCmd(opts, "pacman", "-Sy")
@@ -156,20 +160,3 @@ func (p *Pacman) IsInstalled(pkg string) (bool, error) {
}
return true, nil
}
-
-func (p *Pacman) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
- var cmd *exec.Cmd
- if opts.AsRoot {
- cmd = exec.Command(getRootCmd(p.rootCmd), mgrCmd)
- cmd.Args = append(cmd.Args, opts.Args...)
- cmd.Args = append(cmd.Args, args...)
- } else {
- cmd = exec.Command(mgrCmd, args...)
- }
-
- if opts.NoConfirm {
- cmd.Args = append(cmd.Args, "--noconfirm")
- }
-
- return cmd
-}
diff --git a/pkg/manager/yum.go b/pkg/manager/yum.go
index 65b00e5..2dc50fd 100644
--- a/pkg/manager/yum.go
+++ b/pkg/manager/yum.go
@@ -26,9 +26,16 @@ import (
// YUM represents the YUM package manager
type YUM struct {
+ CommonPackageManager
CommonRPM
+}
- rootCmd string
+func NewYUM() *YUM {
+ return &YUM{
+ CommonPackageManager: CommonPackageManager{
+ noConfirmArg: "-y",
+ },
+ }
}
func (*YUM) Exists() bool {
@@ -44,10 +51,6 @@ func (*YUM) Format() string {
return "rpm"
}
-func (y *YUM) SetRootCmd(s string) {
- y.rootCmd = s
-}
-
func (y *YUM) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := y.getCmd(opts, "yum", "upgrade")
@@ -110,20 +113,3 @@ func (y *YUM) UpgradeAll(opts *Opts) error {
}
return nil
}
-
-func (y *YUM) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
- var cmd *exec.Cmd
- if opts.AsRoot {
- cmd = exec.Command(getRootCmd(y.rootCmd), mgrCmd)
- cmd.Args = append(cmd.Args, opts.Args...)
- cmd.Args = append(cmd.Args, args...)
- } else {
- cmd = exec.Command(mgrCmd, args...)
- }
-
- if opts.NoConfirm {
- cmd.Args = append(cmd.Args, "-y")
- }
-
- return cmd
-}
diff --git a/pkg/manager/zypper.go b/pkg/manager/zypper.go
index bf7a022..7c2ab30 100644
--- a/pkg/manager/zypper.go
+++ b/pkg/manager/zypper.go
@@ -26,8 +26,16 @@ import (
// Zypper represents the Zypper package manager
type Zypper struct {
+ CommonPackageManager
CommonRPM
- rootCmd string
+}
+
+func NewZypper() *YUM {
+ return &YUM{
+ CommonPackageManager: CommonPackageManager{
+ noConfirmArg: "-y",
+ },
+ }
}
func (*Zypper) Exists() bool {
@@ -43,10 +51,6 @@ func (*Zypper) Format() string {
return "rpm"
}
-func (z *Zypper) SetRootCmd(s string) {
- z.rootCmd = s
-}
-
func (z *Zypper) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := z.getCmd(opts, "zypper", "refresh")
@@ -109,20 +113,3 @@ func (z *Zypper) UpgradeAll(opts *Opts) error {
}
return nil
}
-
-func (z *Zypper) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
- var cmd *exec.Cmd
- if opts.AsRoot {
- cmd = exec.Command(getRootCmd(z.rootCmd), mgrCmd)
- cmd.Args = append(cmd.Args, opts.Args...)
- cmd.Args = append(cmd.Args, args...)
- } else {
- cmd = exec.Command(mgrCmd, args...)
- }
-
- if opts.NoConfirm {
- cmd.Args = append(cmd.Args, "-y")
- }
-
- return cmd
-}
diff --git a/pkg/repos/pull.go b/pkg/repos/pull.go
index 835a3d1..aabfbbd 100644
--- a/pkg/repos/pull.go
+++ b/pkg/repos/pull.go
@@ -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),
)
}
diff --git a/repo.go b/repo.go
index b3a6ed5..4a84986 100644
--- a/repo.go
+++ b/repo.go
@@ -20,7 +20,6 @@
package main
import (
- "log/slog"
"os"
"path/filepath"
@@ -28,10 +27,10 @@ import (
"github.com/urfave/cli/v2"
"golang.org/x/exp/slices"
- "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
- database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
+ appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
- "gitea.plemya-x.ru/Plemya-x/ALR/pkg/repos"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
)
func AddRepoCmd() *cli.Command {
@@ -54,27 +53,32 @@ func AddRepoCmd() *cli.Command {
},
},
Action: func(c *cli.Context) error {
- ctx := c.Context
+ if err := utils.ExitIfNotRoot(); err != nil {
+ return err
+ }
name := c.String("name")
repoURL := c.String("url")
- cfg := config.New()
- err := cfg.Load()
+ ctx := c.Context
+
+ deps, err := appbuilder.
+ New(ctx).
+ WithConfig().
+ Build()
if err != nil {
- slog.Error(gotext.Get("Error loading config"), "err", err)
- os.Exit(1)
+ return err
}
+ defer deps.Defer()
+
+ cfg := deps.Cfg
reposSlice := cfg.Repos()
-
for _, repo := range reposSlice {
- if repo.URL == repoURL {
- slog.Error("Repo already exists", "name", repo.Name)
- os.Exit(1)
+ if repo.URL == repoURL || repo.Name == name {
+ return cliutils.FormatCliExit(gotext.Get("Repo %s already exists", repo.Name), nil)
}
}
-
reposSlice = append(reposSlice, types.Repo{
Name: name,
URL: repoURL,
@@ -83,22 +87,23 @@ func AddRepoCmd() *cli.Command {
err = cfg.SaveUserConfig()
if err != nil {
- slog.Error(gotext.Get("Error saving config"), "err", err)
- os.Exit(1)
+ return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
- db := database.New(cfg)
- err = db.Init(ctx)
- if err != nil {
- slog.Error(gotext.Get("Error pulling repos"), "err", err)
+ if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
+ return err
}
- rs := repos.New(cfg, db)
- err = rs.Pull(ctx, cfg.Repos())
+ deps, err = appbuilder.
+ New(ctx).
+ UseConfig(cfg).
+ WithDB().
+ WithReposForcePull().
+ Build()
if err != nil {
- slog.Error(gotext.Get("Error pulling repos"), "err", err)
- os.Exit(1)
+ return err
}
+ defer deps.Defer()
return nil
},
@@ -119,15 +124,24 @@ func RemoveRepoCmd() *cli.Command {
},
},
Action: func(c *cli.Context) error {
+ if err := utils.ExitIfNotRoot(); err != nil {
+ return err
+ }
+
ctx := c.Context
name := c.String("name")
- cfg := config.New()
- err := cfg.Load()
+
+ deps, err := appbuilder.
+ New(ctx).
+ WithConfig().
+ Build()
if err != nil {
- slog.Error(gotext.Get("Error loading config"), "err", err)
- os.Exit(1)
+ return err
}
+ defer deps.Defer()
+
+ cfg := deps.Cfg
found := false
index := 0
@@ -139,33 +153,37 @@ func RemoveRepoCmd() *cli.Command {
}
}
if !found {
- slog.Error(gotext.Get("Repo does not exist"), "name", name)
- os.Exit(1)
+ return cliutils.FormatCliExit(gotext.Get("Repo \"%s\" does not exist", name), nil)
}
cfg.SetRepos(slices.Delete(reposSlice, index, index+1))
err = os.RemoveAll(filepath.Join(cfg.GetPaths().RepoDir, name))
if err != nil {
- slog.Error(gotext.Get("Error removing repo directory"), "err", err)
- os.Exit(1)
+ return cliutils.FormatCliExit(gotext.Get("Error removing repo directory"), err)
}
-
err = cfg.SaveUserConfig()
if err != nil {
- slog.Error(gotext.Get("Error saving config"), "err", err)
- os.Exit(1)
+ return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
- db := database.New(cfg)
- err = db.Init(ctx)
- if err != nil {
- os.Exit(1)
+ if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
+ return err
}
- err = db.DeletePkgs(ctx, "repository = ?", name)
+
+ deps, err = appbuilder.
+ New(ctx).
+ UseConfig(cfg).
+ WithDB().
+ Build()
if err != nil {
- slog.Error(gotext.Get("Error removing packages from database"), "err", err)
- os.Exit(1)
+ return err
+ }
+ defer deps.Defer()
+
+ err = deps.DB.DeletePkgs(ctx, "repository = ?", name)
+ if err != nil {
+ return cliutils.FormatCliExit(gotext.Get("Error removing packages from database"), err)
}
return nil
@@ -179,25 +197,22 @@ func RefreshCmd() *cli.Command {
Usage: gotext.Get("Pull all repositories that have changed"),
Aliases: []string{"ref"},
Action: func(c *cli.Context) error {
- ctx := c.Context
- cfg := config.New()
- err := cfg.Load()
- if err != nil {
- slog.Error(gotext.Get("Error loading config"), "err", err)
- os.Exit(1)
+ if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
+ return err
}
- db := database.New(cfg)
- err = db.Init(ctx)
+ ctx := c.Context
+
+ deps, err := appbuilder.
+ New(ctx).
+ WithConfig().
+ WithDB().
+ WithReposForcePull().
+ Build()
if err != nil {
- os.Exit(1)
- }
- rs := repos.New(cfg, db)
- err = rs.Pull(ctx, cfg.Repos())
- if err != nil {
- slog.Error(gotext.Get("Error pulling repos"), "err", err)
- os.Exit(1)
+ return err
}
+ defer deps.Defer()
return nil
},
}
diff --git a/search.go b/search.go
index e233ca9..4dad507 100644
--- a/search.go
+++ b/search.go
@@ -18,15 +18,15 @@ package main
import (
"fmt"
- "log/slog"
"os"
"text/template"
"github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2"
- "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
- database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
+ appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/search"
)
@@ -63,32 +63,23 @@ func SearchCmd() *cli.Command {
},
},
Action: func(c *cli.Context) error {
+ if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
+ return err
+ }
+
ctx := c.Context
- cfg := config.New()
- err := cfg.Load()
+
+ deps, err := appbuilder.
+ New(ctx).
+ WithConfig().
+ WithDB().
+ Build()
if err != nil {
- slog.Error(gotext.Get("Error loading config"), "err", err)
- os.Exit(1)
+ return err
}
+ defer deps.Defer()
- db := database.New(cfg)
- err = db.Init(ctx)
- defer db.Close()
-
- if err != nil {
- slog.Error(gotext.Get("Error initialization database"), "err", err)
- os.Exit(1)
- }
-
- format := c.String("format")
- var tmpl *template.Template
- if format != "" {
- tmpl, err = template.New("format").Parse(format)
- if err != nil {
- slog.Error(gotext.Get("Error parsing format template"), "err", err)
- os.Exit(1)
- }
- }
+ db := deps.DB
s := search.New(db)
@@ -102,16 +93,23 @@ func SearchCmd() *cli.Command {
Build(),
)
if err != nil {
- slog.Error(gotext.Get("Error parsing format template"), "err", err)
- os.Exit(1)
+ return cliutils.FormatCliExit(gotext.Get("Error while executing search"), err)
+ }
+
+ format := c.String("format")
+ var tmpl *template.Template
+ if format != "" {
+ tmpl, err = template.New("format").Parse(format)
+ if err != nil {
+ return cliutils.FormatCliExit(gotext.Get("Error parsing format template"), err)
+ }
}
for _, dbPkg := range packages {
if tmpl != nil {
err = tmpl.Execute(os.Stdout, dbPkg)
if err != nil {
- slog.Error(gotext.Get("Error executing template"), "err", err)
- os.Exit(1)
+ return cliutils.FormatCliExit(gotext.Get("Error executing template"), err)
}
fmt.Println()
} else {
diff --git a/upgrade.go b/upgrade.go
index 17cfea6..2e12a05 100644
--- a/upgrade.go
+++ b/upgrade.go
@@ -23,21 +23,21 @@ import (
"context"
"fmt"
"log/slog"
- "os"
"github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2"
"go.elara.ws/vercmp"
"golang.org/x/exp/maps"
- "gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
+ "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
+ appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
database "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/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/build"
"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"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/search"
)
@@ -54,66 +54,77 @@ func UpgradeCmd() *cli.Command {
},
},
Action: func(c *cli.Context) error {
+ if err := utils.ExitIfNotRoot(); err != nil {
+ return err
+ }
+
+ if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
+ return err
+ }
+
+ installer, installerClose, err := build.GetSafeInstaller()
+ if err != nil {
+ return err
+ }
+ defer installerClose()
+
+ if err := utils.ExitIfCantSetNoNewPrivs(); err != nil {
+ return err
+ }
+
+ scripter, scripterClose, err := build.GetSafeScriptExecutor()
+ if err != nil {
+ return err
+ }
+ defer scripterClose()
+
ctx := c.Context
- cfg := config.New()
- err := cfg.Load()
+ deps, err := appbuilder.
+ New(ctx).
+ WithConfig().
+ WithDB().
+ WithRepos().
+ WithDistroInfo().
+ WithManager().
+ Build()
if err != nil {
- slog.Error(gotext.Get("Error loading config"), "err", err)
- os.Exit(1)
+ return err
}
+ defer deps.Defer()
- db := database.New(cfg)
- rs := repos.New(cfg, db)
- err = db.Init(ctx)
+ builder, err := build.NewMainBuilder(
+ deps.Cfg,
+ deps.Manager,
+ deps.Repos,
+ scripter,
+ installer,
+ )
if err != nil {
- slog.Error(gotext.Get("Error initialization database"), "err", err)
- os.Exit(1)
+ return err
}
- info, err := distro.ParseOSRelease(ctx)
+ updates, err := checkForUpdates(ctx, deps.Manager, deps.DB, deps.Info)
if err != nil {
- slog.Error(gotext.Get("Error parsing os-release file"), "err", err)
- os.Exit(1)
- }
-
- mgr := manager.Detect()
- if mgr == nil {
- slog.Error(gotext.Get("Unable to detect a supported package manager on the system"))
- os.Exit(1)
- }
-
- if cfg.AutoPull() {
- err = rs.Pull(ctx, cfg.Repos())
- if err != nil {
- slog.Error(gotext.Get("Error pulling repos"), "err", err)
- os.Exit(1)
- }
- }
-
- updates, err := checkForUpdates(ctx, mgr, cfg, db, rs, info)
- if err != nil {
- slog.Error(gotext.Get("Error checking for updates"), "err", err)
- os.Exit(1)
+ return cliutils.FormatCliExit(gotext.Get("Error checking for updates"), err)
}
if len(updates) > 0 {
- builder := build.NewBuilder(
+ err = builder.InstallALRPackages(
ctx,
- types.BuildOpts{
- Manager: mgr,
- Clean: c.Bool("clean"),
- Interactive: c.Bool("interactive"),
+ &build.BuildArgs{
+ Opts: &types.BuildOpts{
+ Clean: c.Bool("clean"),
+ Interactive: c.Bool("interactive"),
+ },
+ Info: deps.Info,
+ PkgFormat_: build.GetPkgFormat(deps.Manager),
},
- rs,
- info,
- cfg,
+ updates,
)
- builder.InstallPkgs(ctx, updates, nil, types.BuildOpts{
- Manager: mgr,
- Clean: c.Bool("clean"),
- Interactive: c.Bool("interactive"),
- })
+ if err != nil {
+ return cliutils.FormatCliExit(gotext.Get("Error checking for updates"), err)
+ }
} else {
slog.Info(gotext.Get("There is nothing to do."))
}
@@ -126,9 +137,7 @@ func UpgradeCmd() *cli.Command {
func checkForUpdates(
ctx context.Context,
mgr manager.Manager,
- cfg *config.ALRConfig,
db *database.Database,
- rs *repos.Repos,
info *distro.OSRelease,
) ([]database.Package, error) {
installed, err := mgr.ListInstalled(nil)