25 Commits

Author SHA1 Message Date
aa08c04e0c fix: use single output format for alt list and alr list -I
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m25s
Update alr-git / changelog (push) Successful in 22s
Create Release / changelog (push) Successful in 2m38s
2025-07-09 20:38:24 +03:00
f42be105ad feat: add import info from alr-repo.toml
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m5s
Update alr-git / changelog (push) Successful in 24s
2025-07-07 17:45:20 +03:00
1cc408ad7d refactor: generate plugin executors 2025-07-07 13:56:09 +03:00
4899e203bb feat: allow finding packages by "{repo}/{pkg}" and "{pkg}+alr-{repo}"
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m32s
Update alr-git / changelog (push) Successful in 23s
2025-07-06 16:47:46 +03:00
67a6cb31de refactor: migrate e2e tests from efficientgo/e2e to capytest
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m55s
Update alr-git / changelog (push) Successful in 24s
2025-07-05 20:50:20 +03:00
5e24940ef8 fix: firejail integration
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m39s
Update alr-git / changelog (push) Successful in 32s
use buildContents for correct handling symlinks
2025-07-03 17:29:53 +03:00
a600feb083 security: update vulnerable packages
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m7s
Update alr-git / changelog (push) Successful in 23s
Create Release / changelog (push) Successful in 2m33s
Vulnerabilities detected by Trivy scan:
- github.com/go-viper/mapstructure/v2 (GHSA-fv92-fjc5-jj9h)
2025-06-30 08:50:36 +03:00
7060e4f551 chore: refactor Makefile with build and install improvements
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m29s
Update alr-git / changelog (push) Successful in 23s
- fix typo in INSTALLED_BIN variable name
- add GENERATE flag to optionally skip go generate
- add CREATE_SYSTEM_RESOURCES flag for user/dir creation control
- make GIT_VERSION optional with ?= operator
- add informative messages for skipped operations
2025-06-30 08:27:14 +03:00
d77ca4c384 feat: config command
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m5s
Update alr-git / changelog (push) Successful in 27s
2025-06-29 21:26:00 +03:00
6355f25089 feat: add ability to remove build_deps
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m36s
Update alr-git / changelog (push) Successful in 26s
2025-06-28 20:19:07 +03:00
a83561b6a5 fix: implement dirlfs to ignore symlinks
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m16s
Update alr-git / changelog (push) Successful in 25s
2025-06-28 01:01:06 +03:00
4b06809a39 fix: quote files-find output and fail on pattern not exists (#123)
All checks were successful
Update alr-git / changelog (push) Successful in 24s
closes #122
closes #121

Reviewed-on: #123
Co-authored-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
Co-committed-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
2025-06-27 20:22:10 +00:00
401c41160c chore: pass all options to download
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m26s
Update alr-git / changelog (push) Successful in 24s
2025-06-25 19:52:54 +03:00
5e1eeabd04 chore: simplify dlcache initialization
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m31s
Update alr-git / changelog (push) Successful in 23s
2025-06-25 19:18:11 +03:00
db19133254 fix: correct handling opts.PostprocDisabled
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m35s
Update alr-git / changelog (push) Successful in 29s
2025-06-25 08:07:58 +03:00
e8202060d8 chore: remove debug slog.Warn
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m0s
Update alr-git / changelog (push) Successful in 26s
Create Release / changelog (push) Successful in 2m54s
2025-06-22 17:27:57 +03:00
c4a92c67d4 fix parsing overrides
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m34s
Update alr-git / changelog (push) Successful in 29s
2025-06-22 12:44:21 +03:00
85878f69d3 feat: add checksum for torrent downloader
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m0s
Update alr-git / changelog (push) Successful in 28s
2025-06-20 20:12:43 +03:00
6bccce1db4 feat: add checksum for git downloader 2025-06-20 19:35:22 +03:00
b5474b1eb4 ci: disable building alr-bin
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 7m5s
Update alr-git / changelog (push) Successful in 30s
2025-06-20 09:21:03 +03:00
51fdea781b fix: correct pull for multiple repos 2025-06-20 09:08:34 +03:00
4c1f2ea90f feat: support mirrors
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 5m45s
Update alr-git / changelog (push) Successful in 25s
2025-06-19 19:00:08 +03:00
7fa7f8ba82 security: update vulnerable packages
All checks were successful
Pre-commit / pre-commit (pull_request) Successful in 6m11s
Update alr-git / changelog (push) Successful in 29s
Vulnerabilities detected by Trivy scan:
- github.com/cloudflare/circl (GHSA-2x5j-vhc8-9cwm)
2025-06-19 12:10:31 +03:00
25d001c1c9 fix: add find-files (#109)
All checks were successful
Update alr-git / changelog (push) Successful in 31s
closes #96

Reviewed-on: #109
Co-authored-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
Co-committed-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
2025-06-19 09:03:37 +00:00
f86b3003b1 fix: add symlink handling in createFirejailedBinary (#108)
All checks were successful
Update alr-git / changelog (push) Successful in 30s
closes #107

Reviewed-on: #108
Co-authored-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
Co-committed-by: Maxim Slipenko <no-reply@maxim.slipenko.com>
2025-06-17 18:56:19 +00:00
89 changed files with 4691 additions and 1683 deletions

View File

@ -84,31 +84,31 @@ jobs:
sed -i "s/version='[0-9]\+\.[0-9]\+\.[0-9]\+'/version='${{ env.VERSION }}'/g" alr-default/alr-bin/alr.sh
sed -i "s/release='[0-9]\+'/release='1'/g" alr-default/alr-bin/alr.sh
- name: Install alr
run: |
make install
# - name: Install alr
# run: |
# make install
#
# # temporary fix
# groupadd wheel
# usermod -aG wheel root
# temporary fix
groupadd wheel
usermod -aG wheel root
# - name: Build packages
# run: |
# SCRIPT_PATH=alr-default/alr-bin/alr.sh
# ALR_DISTRO=altlinux ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH"
# ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH"
# ALR_PKG_FORMAT=deb alr build -s "$SCRIPT_PATH"
# ALR_PKG_FORMAT=archlinux alr build -s "$SCRIPT_PATH"
- name: Build packages
run: |
SCRIPT_PATH=alr-default/alr-bin/alr.sh
ALR_DISTRO=altlinux ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH"
ALR_PKG_FORMAT=rpm alr build -s "$SCRIPT_PATH"
ALR_PKG_FORMAT=deb alr build -s "$SCRIPT_PATH"
ALR_PKG_FORMAT=archlinux alr build -s "$SCRIPT_PATH"
- name: Upload assets
uses: akkuman/gitea-release-action@v1
with:
body: ${{ steps.changes.outputs.changes }}
files: |-
alr-bin+alr-default_${{ env.VERSION }}-1.red80_amd64.deb \
alr-bin+alr-default-${{ env.VERSION }}-1-x86_64.pkg.tar.zst \
alr-bin+alr-default-${{ env.VERSION }}-1.red80.x86_64.rpm \
alr-bin+alr-default-${{ env.VERSION }}-alt1.x86_64.rpm
# - name: Upload assets
# uses: akkuman/gitea-release-action@v1
# with:
# body: ${{ steps.changes.outputs.changes }}
# files: |-
# alr-bin+alr-default_${{ env.VERSION }}-1.red80_amd64.deb \
# alr-bin+alr-default-${{ env.VERSION }}-1-x86_64.pkg.tar.zst \
# alr-bin+alr-default-${{ env.VERSION }}-1.red80.x86_64.rpm \
# alr-bin+alr-default-${{ env.VERSION }}-alt1.x86_64.rpm
- name: Commit changes
run: |

View File

@ -36,11 +36,14 @@ linters:
- unused
- errcheck
- typecheck
# - forbidigo
- wrapcheck
issues:
fix: true
exclude-rules:
- linters:
- wrapcheck
path-except: "internal/repos/find.go"
- path: _test\.go
linters:
- errcheck

View File

@ -1,16 +1,21 @@
NAME := alr
GIT_VERSION = $(shell git describe --tags )
GIT_VERSION ?= $(shell git describe --tags )
IGNORE_ROOT_CHECK ?= 0
DESTDIR ?=
PREFIX ?= /usr/local
BIN := ./$(NAME)
INSTALED_BIN := $(DESTDIR)/$(PREFIX)/bin/$(NAME)
INSTALLED_BIN := $(DESTDIR)/$(PREFIX)/bin/$(NAME)
COMPLETIONS_DIR := ./scripts/completion
BASH_COMPLETION := $(COMPLETIONS_DIR)/bash
ZSH_COMPLETION := $(COMPLETIONS_DIR)/zsh
INSTALLED_BASH_COMPLETION := $(DESTDIR)$(PREFIX)/share/bash-completion/completions/$(NAME)
INSTALLED_ZSH_COMPLETION := $(DESTDIR)$(PREFIX)/share/zsh/site-functions/_$(NAME)
GENERATE ?= 1
CREATE_SYSTEM_RESOURCES ?= 1
ROOT_DIRS := /var/cache/alr /etc/alr
ADD_LICENSE_BIN := go run github.com/google/addlicense@4caba19b7ed7818bb86bc4cd20411a246aa4a524
GOLANGCI_LINT_BIN := go run github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4
XGOTEXT_BIN := go run github.com/Tom5521/xgotext@v1.2.0
@ -21,6 +26,11 @@ build: check-no-root $(BIN)
export CGO_ENABLED := 0
$(BIN):
ifeq ($(GENERATE),1)
go generate ./...
else
@echo "Skipping go generate (GENERATE=0)"
endif
go build -ldflags="-X 'gitea.plemya-x.ru/Plemya-x/ALR/internal/config.Version=$(GIT_VERSION)'" -o $@
check-no-root:
@ -31,20 +41,26 @@ check-no-root:
fi
install: \
$(INSTALED_BIN) \
$(INSTALLED_BIN) \
$(INSTALLED_BASH_COMPLETION) \
$(INSTALLED_ZSH_COMPLETION)
@echo "Installation done!"
$(INSTALED_BIN): $(BIN)
$(INSTALLED_BIN): $(BIN)
install -Dm755 $< $@
setcap cap_setuid,cap_setgid+ep $(INSTALED_BIN)
ifeq ($(CREATE_SYSTEM_RESOURCES),1)
setcap cap_setuid,cap_setgid+ep $(INSTALLED_BIN)
@if id alr >/dev/null 2>&1; then \
echo "User 'alr' already exists. Skipping."; \
else \
useradd -r -s /usr/sbin/nologin alr; \
fi
install -d -o alr -g alr -m 755 /var/cache/alr /etc/alr
@for dir in $(ROOT_DIRS); do \
install -d -o alr -g alr -m 755 $$dir; \
done
else
@echo "Skipping user and root dir creation (CREATE_SYSTEM_RESOURCES=0)"
endif
$(INSTALLED_BASH_COMPLETION): $(BASH_COMPLETION)
install -Dm755 $< $@
@ -54,7 +70,7 @@ $(INSTALLED_ZSH_COMPLETION): $(ZSH_COMPLETION)
uninstall:
rm -f \
$(INSTALED_BIN) \
$(INSTALLED_BIN) \
$(INSTALLED_BASH_COMPLETION) \
$(INSTALLED_ZSH_COMPLETION)

View File

@ -11,7 +11,7 @@
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="33.5" y="15" fill="#010101" fill-opacity=".3">coverage</text>
<text x="33.5" y="14">coverage</text>
<text x="86" y="15" fill="#010101" fill-opacity=".3">19.0%</text>
<text x="86" y="14">19.0%</text>
<text x="86" y="15" fill="#010101" fill-opacity=".3">18.9%</text>
<text x="86" y="14">18.9%</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 926 B

After

Width:  |  Height:  |  Size: 926 B

View File

@ -23,7 +23,6 @@ import (
"log/slog"
"os"
"path/filepath"
"strings"
"github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2"
@ -133,15 +132,7 @@ func BuildCmd() *cli.Command {
// TODO: handle multiple packages
packageInput := c.String("package")
arr := strings.Split(packageInput, "/")
var packageSearch string
if len(arr) == 2 {
packageSearch = arr[1]
} else {
packageSearch = arr[0]
}
pkgs, _, err := deps.Repos.FindPkgs(ctx, []string{packageSearch})
pkgs, _, err := deps.Repos.FindPkgs(ctx, []string{packageInput})
if err != nil {
return cliutils.FormatCliExit("failed to find pkgs", err)
}

227
config.go Normal file
View File

@ -0,0 +1,227 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"fmt"
"strconv"
"strings"
"github.com/goccy/go-yaml"
"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/utils"
)
func ConfigCmd() *cli.Command {
return &cli.Command{
Name: "config",
Usage: gotext.Get("Manage config"),
Subcommands: []*cli.Command{
ShowCmd(),
SetConfig(),
GetConfig(),
},
}
}
func ShowCmd() *cli.Command {
return &cli.Command{
Name: "show",
Usage: gotext.Get("Show config"),
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
return nil
}),
Action: func(c *cli.Context) error {
deps, err := appbuilder.
New(c.Context).
WithConfig().
Build()
if err != nil {
return err
}
defer deps.Defer()
content, err := deps.Cfg.ToYAML()
if err != nil {
return err
}
fmt.Println(content)
return nil
},
}
}
var configKeys = []string{
"rootCmd",
"useRootCmd",
"pagerStyle",
"autoPull",
"logLevel",
"ignorePkgUpdates",
}
func SetConfig() *cli.Command {
return &cli.Command{
Name: "set",
Usage: gotext.Get("Set config value"),
ArgsUsage: gotext.Get("<key> <value>"),
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
if c.Args().Len() == 0 {
for _, key := range configKeys {
fmt.Println(key)
}
return nil
}
return nil
}),
Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 2 {
return cliutils.FormatCliExit("missing args", nil)
}
key := c.Args().Get(0)
value := c.Args().Get(1)
deps, err := appbuilder.
New(c.Context).
WithConfig().
Build()
if err != nil {
return err
}
defer deps.Defer()
switch key {
case "rootCmd":
deps.Cfg.System.SetRootCmd(value)
case "useRootCmd":
boolValue, err := strconv.ParseBool(value)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("invalid boolean value for %s: %s", key, value), err)
}
deps.Cfg.System.SetUseRootCmd(boolValue)
case "pagerStyle":
deps.Cfg.System.SetPagerStyle(value)
case "autoPull":
boolValue, err := strconv.ParseBool(value)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("invalid boolean value for %s: %s", key, value), err)
}
deps.Cfg.System.SetAutoPull(boolValue)
case "logLevel":
deps.Cfg.System.SetLogLevel(value)
case "ignorePkgUpdates":
var updates []string
if value != "" {
updates = strings.Split(value, ",")
for i, update := range updates {
updates[i] = strings.TrimSpace(update)
}
}
deps.Cfg.System.SetIgnorePkgUpdates(updates)
case "repo", "repos":
return cliutils.FormatCliExit(gotext.Get("use 'repo add/remove' commands to manage repositories"), nil)
default:
return cliutils.FormatCliExit(gotext.Get("unknown config key: %s", key), nil)
}
if err := deps.Cfg.System.Save(); err != nil {
return cliutils.FormatCliExit(gotext.Get("failed to save config"), err)
}
fmt.Println(gotext.Get("Successfully set %s = %s", key, value))
return nil
}),
}
}
func GetConfig() *cli.Command {
return &cli.Command{
Name: "get",
Usage: gotext.Get("Get config value"),
ArgsUsage: gotext.Get("<key>"),
BashComplete: cliutils.BashCompleteWithError(func(c *cli.Context) error {
if c.Args().Len() == 0 {
for _, key := range configKeys {
fmt.Println(key)
}
return nil
}
return nil
}),
Action: func(c *cli.Context) error {
deps, err := appbuilder.
New(c.Context).
WithConfig().
Build()
if err != nil {
return err
}
defer deps.Defer()
if c.Args().Len() == 0 {
content, err := deps.Cfg.ToYAML()
if err != nil {
return cliutils.FormatCliExit("failed to serialize config", err)
}
fmt.Print(content)
return nil
}
key := c.Args().Get(0)
switch key {
case "rootCmd":
fmt.Println(deps.Cfg.RootCmd())
case "useRootCmd":
fmt.Println(deps.Cfg.UseRootCmd())
case "pagerStyle":
fmt.Println(deps.Cfg.PagerStyle())
case "autoPull":
fmt.Println(deps.Cfg.AutoPull())
case "logLevel":
fmt.Println(deps.Cfg.LogLevel())
case "ignorePkgUpdates":
updates := deps.Cfg.IgnorePkgUpdates()
if len(updates) == 0 {
fmt.Println("[]")
} else {
fmt.Println(strings.Join(updates, ", "))
}
case "repo", "repos":
repos := deps.Cfg.Repos()
if len(repos) == 0 {
fmt.Println("[]")
} else {
repoData, err := yaml.Marshal(repos)
if err != nil {
return cliutils.FormatCliExit("failed to serialize repos", err)
}
fmt.Print(string(repoData))
}
default:
return cliutils.FormatCliExit(gotext.Get("unknown config key: %s", key), nil)
}
return nil
},
}
}

View File

@ -0,0 +1,5 @@
- name: alr-repo
url: https://gitea.plemya-x.ru/Plemya-x/repo-for-tests
ref: main
mirrors:
- https://github.com/example/example.git

View File

@ -0,0 +1 @@
alr-repo/foo-pkg 1.0.0-1

View File

@ -0,0 +1,2 @@
alr-repo/bar-pkg 1.0.0-1
alr-repo/foo-pkg 1.0.0-1

View File

@ -19,54 +19,24 @@
package e2etests_test
import (
"bytes"
"testing"
"github.com/efficientgo/e2e"
"github.com/stretchr/testify/assert"
"go.alt-gnome.ru/capytest"
)
func TestE2EAlrAddRepo(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"add-repo-remove-repo",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
err := r.Exec(e2e.NewCommand(
"sudo",
"alr",
"addrepo",
"--name",
"alr-repo",
"--url",
"https://gitea.plemya-x.ru/Plemya-x/alr-repo.git",
))
assert.NoError(t, err)
func(t *testing.T, r capytest.Runner) {
execShouldNoError(t, r, "sudo", "alr", "addrepo", "--name", "alr-repo", "--url", "https://gitea.plemya-x.ru/Plemya-x/alr-repo.git")
execShouldNoError(t, r, "bash", "-c", "cat /etc/alr/alr.toml")
execShouldNoError(t, r, "sudo", "alr", "removerepo", "--name", "alr-repo")
err = r.Exec(e2e.NewCommand(
"bash",
"-c",
"cat /etc/alr/alr.toml",
))
assert.NoError(t, err)
err = r.Exec(e2e.NewCommand(
"sudo",
"alr",
"removerepo",
"--name",
"alr-repo",
))
assert.NoError(t, err)
var buf bytes.Buffer
err = r.Exec(e2e.NewCommand(
"bash",
"-c",
"cat /etc/alr/alr.toml",
), e2e.WithExecOptionStdout(&buf))
assert.NoError(t, err)
assert.Contains(t, buf.String(), "rootCmd")
r.Command("bash", "-c", "cat /etc/alr/alr.toml").
ExpectStdoutContains("repo = []").
Run(t)
},
)
}

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EBashCompletion(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"bash-completion",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
execShouldNoError(t, r, "alr", "install", "--generate-bash-completion")
},
)

View File

@ -19,84 +19,13 @@
package e2etests_test
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"log"
"os"
"testing"
"time"
"github.com/efficientgo/e2e"
"github.com/stretchr/testify/assert"
expect "github.com/tailscale/goexpect"
"go.alt-gnome.ru/capytest"
"go.alt-gnome.ru/capytest/providers/podman"
)
// DebugWriter оборачивает io.Writer и логирует все записываемые данные.
type DebugWriter struct {
prefix string
writer io.Writer
}
func (d *DebugWriter) Write(p []byte) (n int, err error) {
log.Printf("%s: Writing data: %q", d.prefix, p) // Логируем данные
return d.writer.Write(p)
}
// DebugReader оборачивает io.Reader и логирует все читаемые данные.
type DebugReader struct {
prefix string
reader io.Reader
}
func (d *DebugReader) Read(p []byte) (n int, err error) {
n, err = d.reader.Read(p)
if n > 0 {
log.Printf("%s: Read data: %q", d.prefix, p[:n]) // Логируем данные
}
return n, err
}
func e2eSpawn(runnable e2e.Runnable, command e2e.Command, timeout time.Duration, opts ...expect.Option) (expect.Expecter, <-chan error, error, *io.PipeWriter) {
resCh := make(chan error)
// Создаем pipe для stdin и stdout
stdinReader, stdinWriter := io.Pipe()
stdoutReader, stdoutWriter := io.Pipe()
debugStdinReader := &DebugReader{prefix: "STDIN", reader: stdinReader}
debugStdoutWriter := &DebugWriter{prefix: "STDOUT", writer: stdoutWriter}
go func() {
err := runnable.Exec(
command,
e2e.WithExecOptionStdout(debugStdoutWriter),
e2e.WithExecOptionStdin(debugStdinReader),
e2e.WithExecOptionStderr(debugStdoutWriter),
)
resCh <- err
}()
exp, chnErr, err := expect.SpawnGeneric(&expect.GenOptions{
In: stdinWriter,
Out: stdoutReader,
Wait: func() error {
return <-resCh
},
Close: func() error {
stdinWriter.Close()
stdoutReader.Close()
return nil
},
Check: func() bool { return true },
}, timeout, expect.Verbose(true), expect.VerboseWriter(os.Stdout))
return exp, chnErr, err, stdinWriter
}
var ALL_SYSTEMS []string = []string{
"ubuntu-24.04",
"alt-sisyphus",
@ -120,71 +49,20 @@ var COMMON_SYSTEMS []string = []string{
"ubuntu-24.04",
}
func dockerMultipleRun(t *testing.T, name string, ids []string, f func(t *testing.T, runnable e2e.Runnable)) {
t.Run(name, func(t *testing.T) {
for _, id := range ids {
t.Run(id, func(t *testing.T) {
t.Parallel()
dockerName := fmt.Sprintf("alr-test-%s-%s", name, id)
hash := sha256.New()
hash.Write([]byte(dockerName))
hashSum := hash.Sum(nil)
hashString := hex.EncodeToString(hashSum)
truncatedHash := hashString[:8]
e, err := e2e.New(e2e.WithVerbose(), e2e.WithName(fmt.Sprintf("alr-%s", truncatedHash)))
assert.NoError(t, err)
t.Cleanup(e.Close)
imageId := fmt.Sprintf("ghcr.io/maks1ms/alr-e2e-test-image-%s", id)
runnable := e.Runnable(dockerName).Init(
e2e.StartOptions{
Image: imageId,
Volumes: []string{
"./alr:/tmp/alr",
},
Privileged: true,
},
)
assert.NoError(t, e2e.StartAndWaitReady(runnable))
err = runnable.Exec(e2e.NewCommand("/bin/alr-test-setup", "alr-install"))
if err != nil {
panic(err)
}
err = runnable.Exec(e2e.NewCommand("/bin/alr-test-setup", "passwordless-sudo-setup"))
if err != nil {
panic(err)
}
f(t, runnable)
})
}
})
func execShouldNoError(t *testing.T, r capytest.Runner, cmd string, args ...string) {
t.Helper()
r.Command(cmd, args...).ExpectSuccess().Run(t)
}
func execShouldNoError(t *testing.T, r e2e.Runnable, cmd string, args ...string) {
assert.NoError(t, r.Exec(e2e.NewCommand(cmd, args...)))
}
func execShouldError(t *testing.T, r e2e.Runnable, cmd string, args ...string) {
assert.Error(t, r.Exec(e2e.NewCommand(cmd, args...)))
}
func runTestCommands(t *testing.T, r e2e.Runnable, timeout time.Duration, expects []expect.Batcher) {
exp, _, err, _ := e2eSpawn(
r,
e2e.NewCommand("/bin/bash"), 25*time.Second,
expect.Verbose(true),
)
assert.NoError(t, err)
_, err = exp.ExpectBatch(
expects,
timeout,
)
assert.NoError(t, err)
func execShouldError(t *testing.T, r capytest.Runner, cmd string, args ...string) {
t.Helper()
r.Command(cmd, args...).ExpectFailure().Run(t)
}
const REPO_NAME_FOR_E2E_TESTS = "alr-repo"
const REPO_URL_FOR_E2E_TESTS = "https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git"
func defaultPrepare(t *testing.T, r e2e.Runnable) {
func defaultPrepare(t *testing.T, r capytest.Runner) {
execShouldNoError(t, r,
"sudo",
"alr",
@ -200,3 +78,19 @@ func defaultPrepare(t *testing.T, r e2e.Runnable) {
"ref",
)
}
func runMatrixSuite(t *testing.T, name string, images []string, test func(t *testing.T, r capytest.Runner)) {
t.Helper()
for _, image := range images {
ts := capytest.NewTestSuite(t, podman.Provider(
podman.WithImage(fmt.Sprintf("ghcr.io/maks1ms/alr-e2e-test-image-%s", image)),
podman.WithVolumes("./alr:/tmp/alr"),
podman.WithPrivileged(true),
))
ts.BeforeEach(func(t *testing.T, r capytest.Runner) {
execShouldNoError(t, r, "/bin/alr-test-setup", "alr-install")
execShouldNoError(t, r, "/bin/alr-test-setup", "passwordless-sudo-setup")
})
ts.Run(fmt.Sprintf("%s/%s", name, image), test)
}
}

View File

@ -22,15 +22,15 @@ import (
"fmt"
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EFirejailedPackage(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"firejailed-package",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "alr", "build", "-p", fmt.Sprintf("%s/firejailed-pkg", REPO_NAME_FOR_E2E_TESTS))
execShouldError(t, r, "alr", "build", "-p", fmt.Sprintf("%s/firejailed-pkg-incorrect", REPO_NAME_FOR_E2E_TESTS))

View File

@ -20,24 +20,15 @@ package e2etests_test
import (
"testing"
"time"
"github.com/efficientgo/e2e"
expect "github.com/tailscale/goexpect"
"go.alt-gnome.ru/capytest"
)
func TestE2EAlrFix(t *testing.T) {
dockerMultipleRun(
t,
"run-fix",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
runTestCommands(t, r, time.Second*30, []expect.Batcher{
&expect.BSnd{S: "alr fix\n"},
&expect.BExp{R: `--> Done`},
&expect.BSnd{S: "echo $?\n"},
&expect.BExp{R: `^0\n$`},
})
},
)
runMatrixSuite(t, "run-fix", COMMON_SYSTEMS, func(t *testing.T, r capytest.Runner) {
r.Command("alr", "fix").
ExpectStderrContains("--> Done").
ExpectSuccess().
Run(t)
})
}

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EGroupAndSummaryField(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"group-and-summary-field",
RPM_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sh", "-c", "alr search --name test-group-and-summary --format \"{{.Group.Resolved}}\" | grep ^System/Base$")
execShouldNoError(t, r, "sh", "-c", "alr search --name test-group-and-summary --format \"{{.Summary.Resolved}}\" | grep \"^Custom summary$\"")

View File

@ -0,0 +1,40 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//go:build e2e
package e2etests_test
import (
"testing"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue129RepoTomlImportTest(t *testing.T) {
runMatrixSuite(
t,
"issue-129-repo-toml-import-test",
COMMON_SYSTEMS,
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
r.Command("alr", "config", "get", "repos").
ExpectStdoutMatchesSnapshot().
Run(t)
},
)
}

View File

@ -0,0 +1,63 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//go:build e2e
package e2etests_test
import (
"fmt"
"testing"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue130Install(t *testing.T) {
runMatrixSuite(
t,
"alr install {repo}/{package}",
COMMON_SYSTEMS,
func(t *testing.T, r capytest.Runner) {
t.Parallel()
defaultPrepare(t, r)
r.Command("sudo", "alr", "in", fmt.Sprintf("%s/foo-pkg", REPO_NAME_FOR_E2E_TESTS)).
ExpectSuccess().
Run(t)
r.Command("sudo", "alr", "in", fmt.Sprintf("%s/bar-pkg", "NOT_REPO_NAME_FOR_E2E_TESTS")).
ExpectFailure().
Run(t)
},
)
runMatrixSuite(
t,
"alr install {package}+alr-{repo}",
COMMON_SYSTEMS,
func(t *testing.T, r capytest.Runner) {
t.Parallel()
defaultPrepare(t, r)
r.Command("sudo", "alr", "in", fmt.Sprintf("foo-pkg+alr-%s", REPO_NAME_FOR_E2E_TESTS)).
ExpectSuccess().
Run(t)
r.Command("sudo", "alr", "in", fmt.Sprintf("bar-pkg+alr-%s", "NOT_REPO_NAME_FOR_E2E_TESTS")).
ExpectFailure().
Run(t)
},
)
}

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue32Interactive(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"issue-32-interactive",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
execShouldNoError(t, r, "alr", "--interactive=false", "remove", "ca-certificates")
execShouldNoError(t, r, "sudo", "alr", "--interactive=false", "remove", "openssl")
execShouldNoError(t, r, "alr", "fix")

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue41AutoreqSkiplist(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"issue-41-autoreq-skiplist",
AUTOREQ_AUTOPROV_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "alr", "build", "-p", "alr-repo/test-autoreq-autoprov")
execShouldNoError(t, r, "sh", "-c", "rpm -qp --requires *.rpm | grep \"^/bin/sh$\"")

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue50InstallMultiple(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"issue-50-install-multiple",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "in", "foo-pkg", "bar-pkg")
execShouldNoError(t, r, "cat", "/opt/foo")

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue53LcAllCInfo(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"issue-53-lc-all-c-info",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "bash", "-c", "LANG=C alr info foo-pkg")
},

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue59RmCompletion(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"issue-59-rm-completion",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "in", "foo-pkg", "bar-pkg")
execShouldNoError(t, r, "sh", "-c", "alr rm --generate-bash-completion | grep ^foo-pkg$")

View File

@ -0,0 +1,50 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//go:build e2e
package e2etests_test
import (
"testing"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue62List(t *testing.T) {
runMatrixSuite(
t,
"issue-62-list",
COMMON_SYSTEMS,
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", "alr-repo", "bd26236cd7")
execShouldNoError(t, r, "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "in", "foo-pkg")
r.Command("alr", "list", "-I").
ExpectSuccess().
ExpectStdoutMatchesSnapshot().
Run(t)
r.Command("alr", "list").
ExpectSuccess().
ExpectStdoutMatchesSnapshot().
Run(t)
},
)
}

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue72InstallWithDeps(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"issue-72-install-with-deps",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "in", "test-app-with-lib")
},

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue74Upgradable(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"issue-74-upgradable",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", "alr-repo", "bd26236cd7")
execShouldNoError(t, r, "alr", "ref")

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue75InstallWithDeps(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"issue-75-ref-specify",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", "alr-repo", "bd26236cd7")
execShouldNoError(t, r, "sh", "-c", "test $(alr list | wc -l) -eq 2 || exit 1")

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func Test75SinglePackageRepo(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"issue-76-single-package-repo",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
execShouldNoError(t, r,
"sudo",
"alr",
@ -38,8 +38,9 @@ func Test75SinglePackageRepo(t *testing.T) {
REPO_NAME_FOR_E2E_TESTS,
"https://gitea.plemya-x.ru/Maks1mS/test-single-package-alr-repo.git",
)
execShouldNoError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "set-ref", REPO_NAME_FOR_E2E_TESTS, "1075c918be")
execShouldNoError(t, r, "alr", "ref")
execShouldNoError(t, r, "alr", "fix")
execShouldNoError(t, r, "sudo", "alr", "in", "test-single-repo")
execShouldNoError(t, r, "sh", "-c", "alr list -U")
execShouldNoError(t, r, "sh", "-c", "test $(alr list -U | wc -l) -eq 0 || exit 1")

View File

@ -0,0 +1,49 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//go:build e2e
package e2etests_test
import (
"testing"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue78Mirrors(t *testing.T) {
runMatrixSuite(t, "issue-78-mirrors", COMMON_SYSTEMS, func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldNoError(t, r, "sudo", "alr", "repo", "set-url", REPO_NAME_FOR_E2E_TESTS, "https://example.com")
execShouldNoError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "clear", REPO_NAME_FOR_E2E_TESTS)
execShouldError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "rm", "--partial", REPO_NAME_FOR_E2E_TESTS, "gitea.plemya-x.ru/Maks1mS")
execShouldError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "rm", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldError(t, r, "sudo", "alr", "ref")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "add", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldNoError(t, r, "sudo", "alr", "repo", "mirror", "rm", REPO_NAME_FOR_E2E_TESTS, "https://gitea.plemya-x.ru/Maks1mS/repo-for-tests.git")
execShouldError(t, r, "sudo", "alr", "ref")
},
)
}

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue81MultiplePackages(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"issue-81-multiple-packages",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sudo", "alr", "in", "first-package-with-dashes")
execShouldNoError(t, r, "cat", "/opt/first-package")

View File

@ -21,15 +21,15 @@ package e2etests_test
import (
"testing"
"github.com/efficientgo/e2e"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue91MultiplePackages(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"issue-91-set-repo-ref",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldError(t, r, "sudo", "alr", "repo", "set-ref")
execShouldError(t, r, "sudo", "alr", "repo", "set-ref", "alr-repo")

View File

@ -23,27 +23,26 @@ import (
"strings"
"testing"
"github.com/efficientgo/e2e"
"github.com/stretchr/testify/assert"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue94TwiceBuild(t *testing.T) {
dockerMultipleRun(
runMatrixSuite(
t,
"issue-94-twice-build",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
var stderr bytes.Buffer
err := r.Exec(
e2e.NewCommand("sudo", "alr", "in", "test-94-app"),
e2e.WithExecOptionStderr(&stderr),
)
assert.NoError(t, err, "command failed")
output := stderr.String()
assert.Equal(t, 1, strings.Count(output, "Building package name=test-94-dep"))
r.Command("sudo", "alr", "in", "test-94-app").
WithCaptureStderr(&stderr).
ExpectSuccess().
Run(t)
assert.Equal(t, 1, strings.Count(stderr.String(), "Building package name=test-94-dep"))
},
)
}

View File

@ -0,0 +1,47 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//go:build e2e
package e2etests_test
import (
"testing"
"go.alt-gnome.ru/capytest"
)
func TestE2EIssue95ConfigCommand(t *testing.T) {
runMatrixSuite(
t,
"issue-95-config-command",
COMMON_SYSTEMS,
func(t *testing.T, r capytest.Runner) {
defaultPrepare(t, r)
execShouldNoError(t, r, "sh", "-c", "alr config show | grep \"autoPull: true\"")
execShouldNoError(t, r, "sh", "-c", "alr config get | grep \"autoPull: true\"")
execShouldError(t, r, "sh", "-c", "cat /etc/alr/alr.toml | grep \"autoPull\"")
execShouldNoError(t, r, "alr", "config", "get", "autoPull")
execShouldError(t, r, "alr", "config", "set", "autoPull")
execShouldNoError(t, r, "sudo", "alr", "config", "set", "autoPull", "false")
execShouldNoError(t, r, "sh", "-c", "alr config show | grep \"autoPull: false\"")
execShouldNoError(t, r, "sh", "-c", "alr config get | grep \"autoPull: false\"")
execShouldNoError(t, r, "sh", "-c", "cat /etc/alr/alr.toml | grep \"autoPull = false\"")
execShouldNoError(t, r, "alr", "config", "set", "autoPull", "true")
execShouldNoError(t, r, "sh", "-c", "cat /etc/alr/alr.toml | grep \"autoPull = true\"")
},
)
}

View File

@ -20,25 +20,16 @@ package e2etests_test
import (
"testing"
"time"
"github.com/efficientgo/e2e"
expect "github.com/tailscale/goexpect"
"go.alt-gnome.ru/capytest"
)
func TestE2EAlrVersion(t *testing.T) {
dockerMultipleRun(
t,
"check-version",
COMMON_SYSTEMS,
func(t *testing.T, r e2e.Runnable) {
runTestCommands(t, r, time.Second*10, []expect.Batcher{
&expect.BSnd{S: "alr version\n"},
&expect.BExp{R: `^v\d+\.\d+\.\d+(?:-\d+-g[a-f0-9]+)?\n$`},
&expect.BSnd{S: "echo $?\n"},
&expect.BExp{R: `^0\n$`},
})
},
)
runMatrixSuite(t, "version", COMMON_SYSTEMS, func(t *testing.T, r capytest.Runner) {
r.Command("alr", "version").
ExpectStderrRegex(`^v\d+\.\d+\.\d+(?:-\d+-g[a-f0-9]+)?\n$`).
ExpectStdoutEmpty().
ExpectSuccess().
Run(t)
})
}

View File

@ -0,0 +1,251 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"log"
"os"
"reflect"
"strings"
"text/template"
)
func resolvedStructGenerator(buf *bytes.Buffer, fields []*ast.Field) {
contentTemplate := template.Must(template.New("").Parse(`
type {{ .EntityNameLower }}Resolved struct {
{{ .StructFields }}
}
`))
var structFieldsBuilder strings.Builder
for _, field := range fields {
for _, name := range field.Names {
// Поле с типом
fieldTypeStr := exprToString(field.Type)
// Структура поля
var buf bytes.Buffer
buf.WriteString("\t")
buf.WriteString(name.Name)
buf.WriteString(" ")
buf.WriteString(fieldTypeStr)
// Обработка json-тега
jsonTag := ""
if field.Tag != nil {
raw := strings.Trim(field.Tag.Value, "`")
tag := reflect.StructTag(raw)
if val := tag.Get("json"); val != "" {
jsonTag = val
}
}
if jsonTag == "" {
jsonTag = strings.ToLower(name.Name)
}
buf.WriteString(fmt.Sprintf(" `json:\"%s\"`", jsonTag))
buf.WriteString("\n")
structFieldsBuilder.Write(buf.Bytes())
}
}
params := struct {
EntityNameLower string
StructFields string
}{
EntityNameLower: "package",
StructFields: structFieldsBuilder.String(),
}
err := contentTemplate.Execute(buf, params)
if err != nil {
log.Fatalf("execute template: %v", err)
}
}
func toResolvedFuncGenerator(buf *bytes.Buffer, fields []*ast.Field) {
contentTemplate := template.Must(template.New("").Parse(`
func {{ .EntityName }}ToResolved(src *{{ .EntityName }}) {{ .EntityNameLower }}Resolved {
return {{ .EntityNameLower }}Resolved{
{{ .Assignments }}
}
}
`))
var assignmentsBuilder strings.Builder
for _, field := range fields {
for _, name := range field.Names {
var assignBuf bytes.Buffer
assignBuf.WriteString("\t\t")
assignBuf.WriteString(name.Name)
assignBuf.WriteString(": ")
if isOverridableField(field.Type) {
assignBuf.WriteString(fmt.Sprintf("src.%s.Resolved()", name.Name))
} else {
assignBuf.WriteString(fmt.Sprintf("src.%s", name.Name))
}
assignBuf.WriteString(",\n")
assignmentsBuilder.Write(assignBuf.Bytes())
}
}
params := struct {
EntityName string
EntityNameLower string
Assignments string
}{
EntityName: "Package",
EntityNameLower: "package",
Assignments: assignmentsBuilder.String(),
}
err := contentTemplate.Execute(buf, params)
if err != nil {
log.Fatalf("execute template: %v", err)
}
}
func resolveFuncGenerator(buf *bytes.Buffer, fields []*ast.Field) {
contentTemplate := template.Must(template.New("").Parse(`
func Resolve{{ .EntityName }}(pkg *{{ .EntityName }}, overrides []string) {
{{.Code}}}
`))
var codeBuilder strings.Builder
for _, field := range fields {
for _, name := range field.Names {
if isOverridableField(field.Type) {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("\t\tpkg.%s.Resolve(overrides)\n", name.Name))
codeBuilder.Write(buf.Bytes())
}
}
}
params := struct {
EntityName string
Code string
}{
EntityName: "Package",
Code: codeBuilder.String(),
}
err := contentTemplate.Execute(buf, params)
if err != nil {
log.Fatalf("execute template: %v", err)
}
}
func main() {
path := os.Getenv("GOFILE")
if path == "" {
log.Fatal("GOFILE must be set")
}
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
if err != nil {
log.Fatalf("parsing file: %v", err)
}
entityName := "Package" // имя структуры, которую анализируем
found := false
fields := make([]*ast.Field, 0)
// Ищем структуру с нужным именем
for _, decl := range node.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
continue
}
for _, spec := range genDecl.Specs {
typeSpec := spec.(*ast.TypeSpec)
if typeSpec.Name.Name != entityName {
continue
}
structType, ok := typeSpec.Type.(*ast.StructType)
if !ok {
continue
}
fields = structType.Fields.List
found = true
}
}
if !found {
log.Fatalf("struct %s not found", entityName)
}
var buf bytes.Buffer
buf.WriteString("// DO NOT EDIT MANUALLY. This file is generated.\n")
buf.WriteString("package alrsh")
resolvedStructGenerator(&buf, fields)
toResolvedFuncGenerator(&buf, fields)
resolveFuncGenerator(&buf, fields)
// Форматируем вывод
formatted, err := format.Source(buf.Bytes())
if err != nil {
log.Fatalf("formatting: %v", err)
}
outPath := strings.TrimSuffix(path, ".go") + "_gen.go"
outFile, err := os.Create(outPath)
if err != nil {
log.Fatalf("create file: %v", err)
}
_, err = outFile.Write(formatted)
if err != nil {
log.Fatalf("writing output: %v", err)
}
outFile.Close()
}
func exprToString(expr ast.Expr) string {
if t, ok := expr.(*ast.IndexExpr); ok {
if ident, ok := t.X.(*ast.Ident); ok && ident.Name == "OverridableField" {
return exprToString(t.Index) // T
}
}
var buf bytes.Buffer
if err := format.Node(&buf, token.NewFileSet(), expr); err != nil {
return "<invalid>"
}
return buf.String()
}
func isOverridableField(expr ast.Expr) bool {
indexExpr, ok := expr.(*ast.IndexExpr)
if !ok {
return false
}
ident, ok := indexExpr.X.(*ast.Ident)
return ok && ident.Name == "OverridableField"
}

View File

@ -0,0 +1,416 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package main
import (
"bytes"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"log"
"os"
"strings"
"text/template"
"unicode"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type MethodInfo struct {
Name string
Params []ParamInfo
Results []ResultInfo
EntityName string
}
type ParamInfo struct {
Name string
Type string
}
type ResultInfo struct {
Name string
Type string
Index int
}
func extractImports(node *ast.File) []string {
var imports []string
for _, imp := range node.Imports {
if imp.Path.Value != "" {
imports = append(imports, imp.Path.Value)
}
}
return imports
}
func output(path string, buf bytes.Buffer) {
formatted, err := format.Source(buf.Bytes())
if err != nil {
log.Fatalf("formatting: %v", err)
}
outPath := strings.TrimSuffix(path, ".go") + "_gen.go"
outFile, err := os.Create(outPath)
if err != nil {
log.Fatalf("create file: %v", err)
}
_, err = outFile.Write(formatted)
if err != nil {
log.Fatalf("writing output: %v", err)
}
outFile.Close()
}
func main() {
path := os.Getenv("GOFILE")
if path == "" {
log.Fatal("GOFILE must be set")
}
if len(os.Args) < 2 {
log.Fatal("At least one entity name must be provided")
}
entityNames := os.Args[1:]
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, path, nil, parser.AllErrors)
if err != nil {
log.Fatalf("parsing file: %v", err)
}
packageName := node.Name.Name
// Find all specified entities
entityData := make(map[string][]*ast.Field)
for _, decl := range node.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok || genDecl.Tok != token.TYPE {
continue
}
for _, spec := range genDecl.Specs {
typeSpec := spec.(*ast.TypeSpec)
for _, entityName := range entityNames {
if typeSpec.Name.Name == entityName {
interfaceType, ok := typeSpec.Type.(*ast.InterfaceType)
if !ok {
log.Fatalf("entity %s is not an interface", entityName)
}
entityData[entityName] = interfaceType.Methods.List
}
}
}
}
// Verify all entities were found
for _, entityName := range entityNames {
if _, found := entityData[entityName]; !found {
log.Fatalf("interface %s not found", entityName)
}
}
var buf bytes.Buffer
buf.WriteString(`
// DO NOT EDIT MANUALLY. This file is generated.
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
`)
buf.WriteString(fmt.Sprintf("package %s\n", packageName))
// Generate base structures for all entities
baseStructs(&buf, entityNames, extractImports(node))
// Generate method-specific code for each entity
for _, entityName := range entityNames {
methods := parseMethodsFromFields(entityName, entityData[entityName])
argsGen(&buf, methods)
}
output(path, buf)
}
func parseMethodsFromFields(entityName string, fields []*ast.Field) []MethodInfo {
var methods []MethodInfo
for _, field := range fields {
if len(field.Names) == 0 {
continue
}
methodName := field.Names[0].Name
funcType, ok := field.Type.(*ast.FuncType)
if !ok {
continue
}
method := MethodInfo{
Name: methodName,
EntityName: entityName,
}
// Parse parameters, excluding context.Context
if funcType.Params != nil {
for i, param := range funcType.Params.List {
paramType := typeToString(param.Type)
// Skip context.Context parameters
if paramType == "context.Context" {
continue
}
if len(param.Names) == 0 {
method.Params = append(method.Params, ParamInfo{
Name: fmt.Sprintf("Arg%d", i),
Type: paramType,
})
} else {
for _, name := range param.Names {
method.Params = append(method.Params, ParamInfo{
Name: cases.Title(language.Und, cases.NoLower).String(name.Name),
Type: paramType,
})
}
}
}
}
// Parse results
if funcType.Results != nil {
resultIndex := 0
for _, result := range funcType.Results.List {
resultType := typeToString(result.Type)
if resultType == "error" {
continue // Skip error in response struct
}
if len(result.Names) == 0 {
method.Results = append(method.Results, ResultInfo{
Name: fmt.Sprintf("Result%d", resultIndex),
Type: resultType,
Index: resultIndex,
})
} else {
for _, name := range result.Names {
method.Results = append(method.Results, ResultInfo{
Name: cases.Title(language.Und, cases.NoLower).String(name.Name),
Type: resultType,
Index: resultIndex,
})
}
}
resultIndex++
}
}
methods = append(methods, method)
}
return methods
}
func argsGen(buf *bytes.Buffer, methods []MethodInfo) {
// Add template functions first
funcMap := template.FuncMap{
"lowerFirst": func(s string) string {
if len(s) == 0 {
return s
}
return strings.ToLower(s[:1]) + s[1:]
},
"zeroValue": func(typeName string) string {
typeName = strings.TrimSpace(typeName)
switch typeName {
case "string":
return "\"\""
case "int", "int8", "int16", "int32", "int64":
return "0"
case "uint", "uint8", "uint16", "uint32", "uint64":
return "0"
case "float32", "float64":
return "0.0"
case "bool":
return "false"
}
if strings.HasPrefix(typeName, "*") {
return "nil"
}
if strings.HasPrefix(typeName, "[]") ||
strings.HasPrefix(typeName, "map[") ||
strings.HasPrefix(typeName, "chan ") {
return "nil"
}
if typeName == "interface{}" {
return "nil"
}
// If external type: pkg.Type
if strings.Contains(typeName, ".") {
return typeName + "{}"
}
// If starts with uppercase — likely struct
if len(typeName) > 0 && unicode.IsUpper(rune(typeName[0])) {
return typeName + "{}"
}
return "nil"
},
}
argsTemplate := template.Must(template.New("args").Funcs(funcMap).Parse(`
{{range .}}
type {{.EntityName}}{{.Name}}Args struct {
{{range .Params}} {{.Name}} {{.Type}}
{{end}}}
type {{.EntityName}}{{.Name}}Resp struct {
{{range .Results}} {{.Name}} {{.Type}}
{{end}}}
func (s *{{.EntityName}}RPC) {{.Name}}(ctx context.Context, {{range $i, $p := .Params}}{{if $i}}, {{end}}{{lowerFirst $p.Name}} {{$p.Type}}{{end}}) ({{range $i, $r := .Results}}{{if $i}}, {{end}}{{$r.Type}}{{end}}{{if .Results}}, {{end}}error) {
var resp *{{.EntityName}}{{.Name}}Resp
err := s.client.Call("Plugin.{{.Name}}", &{{.EntityName}}{{.Name}}Args{
{{range .Params}} {{.Name}}: {{lowerFirst .Name}},
{{end}} }, &resp)
if err != nil {
return {{range $i, $r := .Results}}{{if $i}}, {{end}}{{zeroValue $r.Type}}{{end}}{{if .Results}}, {{end}}err
}
return {{range $i, $r := .Results}}{{if $i}}, {{end}}resp.{{$r.Name}}{{end}}{{if .Results}}, {{end}}nil
}
func (s *{{.EntityName}}RPCServer) {{.Name}}(args *{{.EntityName}}{{.Name}}Args, resp *{{.EntityName}}{{.Name}}Resp) error {
{{if .Results}}{{range $i, $r := .Results}}{{if $i}}, {{end}}{{lowerFirst $r.Name}}{{end}}, err := {{else}}err := {{end}}s.Impl.{{.Name}}(context.Background(),{{range $i, $p := .Params}}{{if $i}}, {{end}}args.{{$p.Name}}{{end}})
if err != nil {
return err
}
{{if .Results}}*resp = {{.EntityName}}{{.Name}}Resp{
{{range .Results}} {{.Name}}: {{lowerFirst .Name}},
{{end}} }
{{else}}*resp = {{.EntityName}}{{.Name}}Resp{}
{{end}}return nil
}
{{end}}
`))
err := argsTemplate.Execute(buf, methods)
if err != nil {
log.Fatalf("execute args template: %v", err)
}
}
func typeToString(expr ast.Expr) string {
switch t := expr.(type) {
case *ast.Ident:
return t.Name
case *ast.StarExpr:
return "*" + typeToString(t.X)
case *ast.ArrayType:
return "[]" + typeToString(t.Elt)
case *ast.SelectorExpr:
xStr := typeToString(t.X)
if xStr == "context" && t.Sel.Name == "Context" {
return "context.Context"
}
return xStr + "." + t.Sel.Name
case *ast.InterfaceType:
return "interface{}"
default:
return "interface{}"
}
}
func baseStructs(buf *bytes.Buffer, entityNames, imports []string) {
// Ensure "context" is included in imports
updatedImports := imports
hasContext := false
for _, imp := range imports {
if strings.Contains(imp, `"context"`) {
hasContext = true
break
}
}
if !hasContext {
updatedImports = append(updatedImports, `"context"`)
}
contentTemplate := template.Must(template.New("").Parse(`
import (
"net/rpc"
"github.com/hashicorp/go-plugin"
{{range .Imports}} {{.}}
{{end}}
)
{{range .EntityNames}}
type {{ . }}Plugin struct {
Impl {{ . }}
}
type {{ . }}RPCServer struct {
Impl {{ . }}
}
type {{ . }}RPC struct {
client *rpc.Client
}
func (p *{{ . }}Plugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &{{ . }}RPC{client: c}, nil
}
func (p *{{ . }}Plugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &{{ . }}RPCServer{Impl: p.Impl}, nil
}
{{end}}
`))
err := contentTemplate.Execute(buf, struct {
EntityNames []string
Imports []string
}{
EntityNames: entityNames,
Imports: updatedImports,
})
if err != nil {
log.Fatalf("execute template: %v", err)
}
}

49
go.mod
View File

@ -1,44 +1,46 @@
module gitea.plemya-x.ru/Plemya-x/ALR
go 1.23.0
toolchain go1.24.2
go 1.24.4
require (
gitea.plemya-x.ru/Plemya-x/fakeroot v0.0.2-0.20250408104831-427aaa7713c3
github.com/AlecAivazis/survey/v2 v2.3.7
github.com/PuerkitoBio/purell v1.2.0
github.com/alecthomas/chroma/v2 v2.9.1
github.com/caarlos0/env v3.5.0+incompatible
github.com/bmatcuk/doublestar/v4 v4.8.1
github.com/charmbracelet/bubbles v0.20.0
github.com/charmbracelet/bubbletea v1.2.4
github.com/charmbracelet/lipgloss v1.0.0
github.com/charmbracelet/log v0.4.0
github.com/efficientgo/e2e v0.14.1-0.20240418111536-97db25a0c6c0
github.com/go-git/go-billy/v5 v5.6.0
github.com/go-git/go-git/v5 v5.13.0
github.com/goccy/go-yaml v1.18.0
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
github.com/goreleaser/nfpm/v2 v2.41.0
github.com/hashicorp/go-hclog v0.14.1
github.com/hashicorp/go-plugin v1.6.3
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08
github.com/jmoiron/sqlx v1.3.5
github.com/knadh/koanf/parsers/toml/v2 v2.2.0
github.com/knadh/koanf/providers/confmap v1.0.0
github.com/knadh/koanf/providers/env v1.1.0
github.com/knadh/koanf/providers/file v1.2.0
github.com/knadh/koanf/v2 v2.2.1
github.com/leonelquinteros/gotext v1.7.0
github.com/mattn/go-isatty v0.0.20
github.com/mholt/archiver/v4 v4.0.0-alpha.8
github.com/mitchellh/mapstructure v1.5.0
github.com/muesli/reflow v0.3.0
github.com/pelletier/go-toml/v2 v2.1.0
github.com/pelletier/go-toml/v2 v2.2.4
github.com/stretchr/testify v1.10.0
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
github.com/urfave/cli/v2 v2.25.7
github.com/vmihailenco/msgpack/v5 v5.3.5
go.alt-gnome.ru/capytest v0.0.3-0.20250706082755-f20413e052f9
go.alt-gnome.ru/capytest/providers/podman v0.0.3-0.20250706082755-f20413e052f9
go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4
golang.org/x/crypto v0.36.0
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
golang.org/x/sys v0.31.0
golang.org/x/sys v0.33.0
golang.org/x/text v0.23.0
gopkg.in/yaml.v3 v3.0.1
modernc.org/sqlite v1.25.0
mvdan.cc/sh/v3 v3.10.0
xorm.io/xorm v1.3.9
@ -62,7 +64,7 @@ require (
github.com/charmbracelet/harmonica v0.2.0 // indirect
github.com/charmbracelet/x/ansi v0.4.5 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/cloudflare/circl v1.3.8 // indirect
github.com/cloudflare/circl v1.6.1 // indirect
github.com/connesc/cipherio v0.2.1 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/creack/pty v1.1.24 // indirect
@ -71,20 +73,23 @@ require (
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dsnet/compress v0.0.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/efficientgo/core v1.0.0-rc.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/fatih/color v1.7.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/gkampitakis/ciinfo v0.3.2 // indirect
github.com/gkampitakis/go-diff v1.3.2 // indirect
github.com/gkampitakis/go-snaps v0.5.13 // indirect
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
github.com/go-logfmt/logfmt v0.6.0 // indirect
github.com/go-viper/mapstructure/v2 v2.3.0 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.8.1 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect
github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/goreleaser/chglog v0.6.1 // indirect
github.com/goreleaser/fileglob v1.3.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
@ -98,7 +103,11 @@ require (
github.com/kevinburke/ssh_config v1.2.0 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/knadh/koanf/maps v0.1.2 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/maruel/natural v1.1.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
@ -117,13 +126,18 @@ require (
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/skeema/knownhosts v1.3.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/cast v1.7.1 // indirect
github.com/syndtr/goleveldb v1.0.0 // indirect
github.com/therootcompany/xz v1.0.1 // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/ulikunitz/xz v0.5.12 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
@ -135,10 +149,11 @@ require (
golang.org/x/sync v0.12.0 // indirect
golang.org/x/term v0.30.0 // indirect
golang.org/x/tools v0.23.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 // indirect
google.golang.org/grpc v1.58.3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 // indirect
google.golang.org/grpc v1.67.3 // indirect
google.golang.org/protobuf v1.36.1 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect
modernc.org/cc/v3 v3.40.0 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect

119
go.sum
View File

@ -63,10 +63,10 @@ github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPd
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4pJSv7WO+VECIWUQ7OJYSoTrMh4=
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
github.com/bmatcuk/doublestar/v4 v4.8.1 h1:54Bopc5c2cAvhLRAzqOGCYHYyhcDHsFF4wWIR5wKP38=
github.com/bmatcuk/doublestar/v4 v4.8.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
github.com/bodgit/plumbing v1.2.0 h1:gg4haxoKphLjml+tgnecR4yLBV5zo4HAZGCtAh3xCzM=
github.com/bodgit/plumbing v1.2.0/go.mod h1:b9TeRi7Hvc6Y05rjm8VML3+47n4XTZPtQ/5ghqic2n8=
github.com/bodgit/sevenzip v1.3.0 h1:1ljgELgtHqvgIp8W8kgeEGHIWP4ch3xGI8uOBZgLVKY=
@ -75,15 +75,11 @@ github.com/bodgit/windows v1.0.0 h1:rLQ/XjsleZvx4fR1tB/UxQrK+SJ2OFHzfPjLWWOhDIA=
github.com/bodgit/windows v1.0.0/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
github.com/caarlos0/env v3.5.0+incompatible h1:Yy0UN8o9Wtr/jGHZDpCBLpNrzcFLLM2yixi/rBrKyJs=
github.com/caarlos0/env v3.5.0+incompatible/go.mod h1:tdCsowwCzMLdkqRYDlHpZCp2UooDD3MspDBjZ2AD02Y=
github.com/caarlos0/testfs v0.4.4 h1:3PHvzHi5Lt+g332CiShwS8ogTgS3HjrmzZxCm6JCDr8=
github.com/caarlos0/testfs v0.4.4/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk=
github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE=
github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU=
github.com/charmbracelet/bubbletea v1.2.4 h1:KN8aCViA0eps9SCOThb2/XPIlea3ANJLUkv3KnQRNCE=
@ -102,12 +98,13 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI=
github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU=
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEycw=
github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA=
github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
@ -123,10 +120,6 @@ github.com/dsnet/compress v0.0.1/go.mod h1:Aw8dCMJ7RioblQeTqt88akK31OvO8Dhf5Jflh
github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/efficientgo/core v1.0.0-rc.0 h1:jJoA0N+C4/knWYVZ6GrdHOtDyrg8Y/TR4vFpTaqTsqs=
github.com/efficientgo/core v1.0.0-rc.0/go.mod h1:kQa0V74HNYMfuJH6jiPiwNdpWXl4xd/K4tzlrcvYDQI=
github.com/efficientgo/e2e v0.14.1-0.20240418111536-97db25a0c6c0 h1:C/FNIs+MtAJgQYLJ9FX/ACFYyDRuLYoXTmueErrOJyA=
github.com/efficientgo/e2e v0.14.1-0.20240418111536-97db25a0c6c0/go.mod h1:plsKU0YHE9uX+7utvr7SiDtVBSHJyEfHRO4UnUgDmts=
github.com/elazarl/goproxy v1.2.1 h1:njjgvO6cRG9rIqN2ebkqy6cQz2Njkx7Fsfv/zIZqgug=
github.com/elazarl/goproxy v1.2.1/go.mod h1:YfEbZtqP4AetfO6d40vWchF3znWX7C7Vd6ZMfdL8z64=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
@ -140,6 +133,14 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs=
github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo=
github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M=
github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk=
github.com/gkampitakis/go-snaps v0.5.13 h1:Hhjmvv1WboSCxkR9iU2mj5PQ8tsz/y8ECGrIbjjPF8Q=
github.com/gkampitakis/go-snaps v0.5.13/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
@ -156,13 +157,16 @@ github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-viper/mapstructure/v2 v2.3.0 h1:27XbWsHIqhbdR5TIC911OfYvgSaW93HM+dX7970Q7jk=
github.com/go-viper/mapstructure/v2 v2.3.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI=
github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@ -195,8 +199,6 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f h1:5CjVwnuUcp5adK4gmY6i72gpVFVnZDP2h5TmPScB6u4=
github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
@ -209,8 +211,8 @@ github.com/google/rpmpack v0.6.1-0.20240329070804-c2247cbb881a/go.mod h1:uqVAUVQ
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
@ -251,10 +253,6 @@ github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3m
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g=
github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ=
github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@ -272,6 +270,18 @@ github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90
github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo=
github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI=
github.com/knadh/koanf/parsers/toml/v2 v2.2.0 h1:2nV7tHYJ5OZy2BynQ4mOJ6k5bDqbbCzRERLUKBytz3A=
github.com/knadh/koanf/parsers/toml/v2 v2.2.0/go.mod h1:JpjTeK1Ge1hVX0wbof5DMCuDBriR8bWgeQP98eeOZpI=
github.com/knadh/koanf/providers/confmap v1.0.0 h1:mHKLJTE7iXEys6deO5p6olAiZdG5zwp8Aebir+/EaRE=
github.com/knadh/koanf/providers/confmap v1.0.0/go.mod h1:txHYHiI2hAtF0/0sCmcuol4IDcuQbKTybiB1nOcUo1A=
github.com/knadh/koanf/providers/env v1.1.0 h1:U2VXPY0f+CsNDkvdsG8GcsnK4ah85WwWyJgef9oQMSc=
github.com/knadh/koanf/providers/env v1.1.0/go.mod h1:QhHHHZ87h9JxJAn2czdEl6pdkNnDh/JS1Vtsyt65hTY=
github.com/knadh/koanf/providers/file v1.2.0 h1:hrUJ6Y9YOA49aNu/RSYzOTFlqzXSCpmYIDXI7OJU6+U=
github.com/knadh/koanf/providers/file v1.2.0/go.mod h1:bp1PM5f83Q+TOUu10J/0ApLBd9uIzg+n9UgthfY+nRA=
github.com/knadh/koanf/v2 v2.2.1 h1:jaleChtw85y3UdBnI0wCqcg1sj1gPoz6D3caGNHtrNE=
github.com/knadh/koanf/v2 v2.2.1/go.mod h1:PSFru3ufQgTsI7IF+95rf9s8XA1+aHxKuO/W+dPoHEY=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@ -281,11 +291,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leonelquinteros/gotext v1.7.0 h1:jcJmF4AXqyamP7vuw2MMIKs+O3jAEmvrc5JQiI8Ht/8=
github.com/leonelquinteros/gotext v1.7.0/go.mod h1:qJdoQuERPpccw7L70uoU+K/BvTfRBHYsisCQyFLXyvw=
github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw=
github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE=
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
@ -302,11 +311,8 @@ github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+Ei
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/mholt/archiver/v4 v4.0.0-alpha.8 h1:tRGQuDVPh66WCOelqe6LIGh0gwmfwxUrSSDunscGsRM=
@ -332,8 +338,6 @@ github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU=
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2 h1:e3mzJFJs4k83GXBEiTaQ5HgSc/kOK8q0rDaRO0MPaOk=
github.com/nwaples/rardecode/v2 v2.0.0-beta.2/go.mod h1:yntwv/HfMc/Hbvtq9I19D1n58te3h6KsqCf3GxyfBGY=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
@ -344,25 +348,18 @@ github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk=
github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.36.0 h1:78hJTing+BLYLjhXE+Z2BubeEymH5Lr0/Mt8FKkxxYo=
github.com/prometheus/common v0.36.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA=
github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU=
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
@ -371,6 +368,7 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
@ -391,27 +389,31 @@ github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+
github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY=
github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw=
github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc=
github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
@ -430,6 +432,12 @@ github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsr
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/YjuiKhqhGlOCwlIV8SqqGh8=
gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0=
go.alt-gnome.ru/capytest v0.0.2 h1:clmvIqmYS86hhA1rsvivSSPpfOFkJTpbn38EQP7I3E8=
go.alt-gnome.ru/capytest v0.0.2/go.mod h1:lvxPx3H6h+LPnStBFblgoT2wkjv0wbug3S14troykEg=
go.alt-gnome.ru/capytest v0.0.3-0.20250706082755-f20413e052f9 h1:NST+V5LV/eLgs0p6PsuvfHiZ4UrIWqftCdifO8zgg0g=
go.alt-gnome.ru/capytest v0.0.3-0.20250706082755-f20413e052f9/go.mod h1:qiM8LARP+JBZr5mrDoVylOoqjrN0MAzvZ21NR9qMc0Y=
go.alt-gnome.ru/capytest/providers/podman v0.0.3-0.20250706082755-f20413e052f9 h1:VZclgdJxARvhZ6PIWWW2hQ6Ge4XeE36pzUr/U/y62bE=
go.alt-gnome.ru/capytest/providers/podman v0.0.3-0.20250706082755-f20413e052f9/go.mod h1:Wpq1Ny3eMzADJpMJArA2TZGZbsviUBmawtEPcxnoerg=
go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4 h1:Ep54XceQlKhcCHl9awG+wWP4kz4kIP3c3Lzw/Gc/zwY=
go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4/go.mod h1:/7PNW7nFnDR5W7UXZVc04gdVLR/wBNgkm33KgIz0OBk=
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
@ -503,8 +511,6 @@ golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4Iltr
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.10.0 h1:zHCpF2Khkwy4mMB4bv0U37YtJdTGW8jI0glAApi0Kh8=
golang.org/x/oauth2 v0.10.0/go.mod h1:kTpgurOux7LqtuxjuyZa4Gj2gdezIt/jQtGnNFfypQI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -542,8 +548,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
@ -604,8 +610,6 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
@ -619,8 +623,8 @@ google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvx
google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98 h1:bVf09lpb+OJbByTj913DRJioFFAjf/ZGxEz7MajTp2U=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230711160842-782d3b101e98/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8 h1:TqExAhdPaB60Ux47Cn0oLV07rGnxZzIsaRhQaqS666A=
google.golang.org/genproto/googleapis/rpc v0.0.0-20241223144023-3abc09e42ca8/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@ -628,8 +632,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.58.3 h1:BjnpXut1btbtgN/6sp+brB2Kbm2LjNXnidYujAVbSoQ=
google.golang.org/grpc v1.58.3/go.mod h1:tgX3ZQDlNJGU96V6yHh1T/JeoBQ2TXdr43YbYSsCJk0=
google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
@ -649,7 +653,6 @@ gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRN
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

44
info.go
View File

@ -23,10 +23,10 @@ import (
"fmt"
"os"
"github.com/goccy/go-yaml"
"github.com/jeandeaual/go-locale"
"github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v3"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder"
@ -121,35 +121,27 @@ func InfoCmd() *cli.Command {
systemLang = "en"
}
if !all {
info, err := distro.ParseOSRelease(ctx)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error parsing os-release file"), err)
}
names, err = overrides.Resolve(
info,
overrides.DefaultOpts.
WithLanguages([]string{systemLang}),
)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error resolving overrides"), err)
}
info, err := distro.ParseOSRelease(ctx)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error parsing os-release file"), err)
}
names, err = overrides.Resolve(
info,
overrides.DefaultOpts.
WithLanguages([]string{systemLang}),
)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error resolving overrides"), err)
}
for _, pkg := range pkgs {
if !all {
alrsh.ResolvePackage(&pkg, names)
err = yaml.NewEncoder(os.Stdout).Encode(pkg)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error encoding script variables"), err)
}
} else {
err = yaml.NewEncoder(os.Stdout).Encode(pkg)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error encoding script variables"), err)
}
alrsh.ResolvePackage(&pkg, names)
view := alrsh.NewPackageView(pkg)
view.Resolved = !all
err = yaml.NewEncoder(os.Stdout, yaml.UseJSONMarshaler(), yaml.OmitEmpty()).Encode(view)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error encoding script variables"), err)
}
fmt.Println("---")
}

View File

@ -84,6 +84,43 @@ func InternalBuildCmd() *cli.Command {
}
}
func InternalReposCmd() *cli.Command {
return &cli.Command{
Name: "_internal-repos",
HideHelp: true,
Hidden: true,
Action: utils.RootNeededAction(func(ctx *cli.Context) error {
logger.SetupForGoPlugin()
if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil {
return err
}
deps, err := appbuilder.
New(ctx.Context).
WithConfig().
WithDB().
WithReposNoPull().
Build()
if err != nil {
return err
}
defer deps.Defer()
pluginCfg := build.GetPluginServeCommonConfig()
pluginCfg.Plugins = map[string]plugin.Plugin{
"repos": &build.ReposExecutorPlugin{
Impl: build.NewRepos(
deps.Repos,
),
},
}
plugin.Serve(pluginCfg)
return nil
}),
}
}
func InternalInstallCmd() *cli.Command {
return &cli.Command{
Name: "_internal-installer",
@ -125,7 +162,7 @@ func InternalInstallCmd() *cli.Command {
plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: build.HandshakeConfig,
Plugins: map[string]plugin.Plugin{
"installer": &build.InstallerPlugin{
"installer": &build.InstallerExecutorPlugin{
Impl: build.NewInstaller(
manager.Detect(),
),

View File

@ -176,25 +176,6 @@ type ScriptResolverExecutor interface {
ResolveScript(ctx context.Context, pkg *alrsh.Package) *ScriptInfo
}
type ScriptExecutor interface {
ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error)
ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error)
PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error
ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *alrsh.ScriptFile,
varsOfPackages []*alrsh.Package,
repoDeps []string,
builtDeps []*BuiltDep,
basePkg string,
) ([]*BuiltDep, error)
}
type CacheExecutor interface {
CheckForBuiltPackage(ctx context.Context, input *BuildInput, vars *alrsh.Package) (string, bool, error)
}
@ -211,12 +192,6 @@ type CheckerExecutor interface {
) (bool, error)
}
type InstallerExecutor interface {
InstallLocal(paths []string, opts *manager.Opts) error
Install(pkgs []string, opts *manager.Opts) error
RemoveAlreadyInstalled(pkgs []string) ([]string, error)
}
type SourcesInput struct {
Sources []string
Checksums []string
@ -367,7 +342,7 @@ func (b *Builder) BuildPackage(
}
slog.Debug("ViewScript")
slog.Debug("", "varsOfPackages", varsOfPackages)
slog.Debug("", "varsOfPackages", varsOfPackages[0])
err = b.scriptViewerExecutor.ViewScript(ctx, input, sf, basePkg)
if err != nil {
return nil, err
@ -408,7 +383,7 @@ func (b *Builder) BuildPackage(
sources, checksums = removeDuplicatesSources(sources, checksums)
slog.Debug("installBuildDeps")
alrBuildDeps, err := b.installBuildDeps(ctx, input, buildDepends)
alrBuildDeps, installedBuildDeps, err := b.installBuildDeps(ctx, input, buildDepends)
if err != nil {
return nil, err
}
@ -477,9 +452,40 @@ func (b *Builder) BuildPackage(
builtDeps = removeDuplicates(append(builtDeps, res...))
err = b.removeBuildDeps(ctx, input, installedBuildDeps)
if err != nil {
return nil, err
}
return builtDeps, nil
}
func (b *Builder) removeBuildDeps(ctx context.Context, input interface {
BuildOptsProvider
}, deps []string,
) error {
if len(deps) > 0 {
remove, err := cliutils.YesNoPrompt(ctx, gotext.Get("Would you like to remove the build dependencies?"), input.BuildOpts().Interactive, false)
if err != nil {
return err
}
if remove {
err = b.installerExecutor.Remove(
ctx,
deps,
&manager.Opts{
NoConfirm: !input.BuildOpts().Interactive,
},
)
if err != nil {
return err
}
}
}
return nil
}
type InstallPkgsArgs struct {
BuildArgs
AlrPkgs []alrsh.Package
@ -513,6 +519,7 @@ func (b *Builder) InstallALRPackages(
}
err = b.installerExecutor.InstallLocal(
ctx,
GetBuiltPaths(res),
&manager.Opts{
NoConfirm: !input.BuildOpts().Interactive,
@ -608,20 +615,22 @@ func (i *Builder) installBuildDeps(
PkgFormatProvider
},
pkgs []string,
) ([]*BuiltDep, error) {
) ([]*BuiltDep, []string, error) {
var builtDeps []*BuiltDep
var deps []string
var err error
if len(pkgs) > 0 {
deps, err := i.installerExecutor.RemoveAlreadyInstalled(pkgs)
deps, err = i.installerExecutor.RemoveAlreadyInstalled(ctx, pkgs)
if err != nil {
return nil, err
return nil, nil, err
}
builtDeps, err = i.InstallPkgs(ctx, input, deps) // Устанавливаем выбранные пакеты
if err != nil {
return nil, err
return nil, nil, err
}
}
return builtDeps, nil
return builtDeps, deps, nil
}
func (i *Builder) installOptDeps(
@ -634,7 +643,7 @@ func (i *Builder) installOptDeps(
pkgs []string,
) ([]*BuiltDep, error) {
var builtDeps []*BuiltDep
optDeps, err := i.installerExecutor.RemoveAlreadyInstalled(pkgs)
optDeps, err := i.installerExecutor.RemoveAlreadyInstalled(ctx, pkgs)
if err != nil {
return nil, err
}
@ -676,7 +685,7 @@ func (i *Builder) InstallPkgs(
}
if len(builtDeps) > 0 {
err = i.installerExecutor.InstallLocal(GetBuiltPaths(builtDeps), &manager.Opts{
err = i.installerExecutor.InstallLocal(ctx, GetBuiltPaths(builtDeps), &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive,
})
if err != nil {
@ -685,7 +694,7 @@ func (i *Builder) InstallPkgs(
}
if len(repoDeps) > 0 {
err = i.installerExecutor.Install(repoDeps, &manager.Opts{
err = i.installerExecutor.Install(ctx, repoDeps, &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive,
})
if err != nil {

View File

@ -28,7 +28,6 @@ import (
"github.com/goreleaser/nfpm/v2/files"
"github.com/leonelquinteros/gotext"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/osutils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
)
@ -51,6 +50,92 @@ var binaryDirectories = []string{
"/usr/local/bin/",
}
func moveWithSymlinkHandling(src, dst string) error {
srcInfo, err := os.Lstat(src)
if err != nil {
return fmt.Errorf("failed to get source info: %w", err)
}
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
}
if srcInfo.Mode()&os.ModeSymlink != 0 {
return moveSymlink(src, dst)
}
if err := os.Rename(src, dst); err != nil {
return copyAndRemove(src, dst)
}
return nil
}
func moveSymlink(src, dst string) error {
target, err := os.Readlink(src)
if err != nil {
return fmt.Errorf("failed to read symlink: %w", err)
}
if err := os.Symlink(target, dst); err != nil {
return fmt.Errorf("failed to create symlink: %w", err)
}
if err := os.Remove(src); err != nil {
os.Remove(dst)
return fmt.Errorf("failed to remove original symlink: %w", err)
}
return nil
}
func copyAndRemove(src, dst string) error {
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("failed to open source: %w", err)
}
defer srcFile.Close()
dstFile, err := os.Create(dst)
if err != nil {
return fmt.Errorf("failed to create destination: %w", err)
}
defer dstFile.Close()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return fmt.Errorf("failed to copy content: %w", err)
}
srcInfo, err := srcFile.Stat()
if err != nil {
return fmt.Errorf("failed to get source stats: %w", err)
}
if err := dstFile.Chmod(srcInfo.Mode()); err != nil {
return fmt.Errorf("failed to set permissions: %w", err)
}
if err := os.Remove(src); err != nil {
return fmt.Errorf("failed to remove source: %w", err)
}
return nil
}
func moveFileWithErrorHandling(src, dst string) error {
err := moveWithSymlinkHandling(src, dst)
if err != nil {
if os.IsPermission(err) {
return fmt.Errorf("permission denied: %w", err)
}
if os.IsNotExist(err) {
return fmt.Errorf("source file does not exist: %w", err)
}
return fmt.Errorf("failed to move file: %w", err)
}
return nil
}
func applyFirejailIntegration(
vars *alrsh.Package,
dirs types.Directories,
@ -143,48 +228,22 @@ func createFirejailedBinary(
return nil, err
}
if err := osutils.Move(content.Source, filepath.Join(dirs.PkgDir, origFilePath)); err != nil {
if err := moveFileWithErrorHandling(filepath.Join(dirs.PkgDir, content.Destination), filepath.Join(dirs.PkgDir, origFilePath)); err != nil {
return nil, fmt.Errorf("failed to move original binary: %w", err)
}
content.Type = "file"
content.Source = filepath.Join(dirs.PkgDir, content.Destination)
// Create wrapper script
if err := createWrapperScript(content.Source, origFilePath, dest); err != nil {
if err := createWrapperScript(filepath.Join(dirs.PkgDir, content.Destination), origFilePath, dest); err != nil {
return nil, fmt.Errorf("failed to create wrapper script: %w", err)
}
profile, err := getContentFromPath(dest, dirs.PkgDir)
if err != nil {
return nil, err
}
bin, err := getContentFromPath(origFilePath, dirs.PkgDir)
if err != nil {
return nil, err
}
return []*files.Content{
bin,
profile,
}, nil
}
func getContentFromPath(path, base string) (*files.Content, error) {
absPath := filepath.Join(base, path)
fi, err := os.Lstat(absPath)
if err != nil {
return nil, fmt.Errorf("failed to get file info: %w", err)
}
return &files.Content{
Source: absPath,
Destination: path,
FileInfo: &files.ContentFileInfo{
MTime: fi.ModTime(),
Mode: fi.Mode(),
Size: fi.Size(),
},
}, nil
return buildContents(pkg, dirs, &[]string{
origFilePath,
dest,
})
}
func generateSafeName(destination string) (string, error) {

View File

@ -138,7 +138,10 @@ func TestCreateFirejailedBinary(t *testing.T) {
os.MkdirAll(pkgDir, 0o755)
os.MkdirAll(scriptDir, 0o755)
srcBinary := filepath.Join(tmpDir, "test-binary")
binDir := filepath.Join(pkgDir, "usr", "bin")
os.MkdirAll(binDir, 0o755)
srcBinary := filepath.Join(binDir, "test-binary")
os.WriteFile(srcBinary, []byte("#!/bin/bash\necho test"), 0o755)
defaultProfile := filepath.Join(scriptDir, "default.profile")
@ -154,7 +157,7 @@ func TestCreateFirejailedBinary(t *testing.T) {
content := &files.Content{
Source: srcBinary,
Destination: "./usr/bin/test-binary",
Destination: "/usr/bin/test-binary",
Type: "file",
}
@ -172,7 +175,10 @@ func TestCreateFirejailedBinary(t *testing.T) {
os.MkdirAll(pkgDir, 0o755)
os.MkdirAll(scriptDir, 0o755)
srcBinary := filepath.Join(tmpDir, "special-binary")
binDir := filepath.Join(pkgDir, "usr", "bin")
os.MkdirAll(binDir, 0o755)
srcBinary := filepath.Join(binDir, "special-binary")
os.WriteFile(srcBinary, []byte("#!/bin/bash\necho special"), 0o755)
defaultProfile := filepath.Join(scriptDir, "default.profile")
@ -191,7 +197,7 @@ func TestCreateFirejailedBinary(t *testing.T) {
content := &files.Content{
Source: srcBinary,
Destination: "./usr/bin/special-binary",
Destination: "/usr/bin/special-binary",
Type: "file",
}

View File

@ -17,6 +17,8 @@
package build
import (
"context"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
)
@ -28,15 +30,19 @@ func NewInstaller(mgr manager.Manager) *Installer {
type Installer struct{ mgr manager.Manager }
func (i *Installer) InstallLocal(paths []string, opts *manager.Opts) error {
func (i *Installer) InstallLocal(ctx context.Context, paths []string, opts *manager.Opts) error {
return i.mgr.InstallLocal(opts, paths...)
}
func (i *Installer) Install(pkgs []string, opts *manager.Opts) error {
func (i *Installer) Install(ctx context.Context, pkgs []string, opts *manager.Opts) error {
return i.mgr.Install(opts, pkgs...)
}
func (i *Installer) RemoveAlreadyInstalled(pkgs []string) ([]string, error) {
func (i *Installer) Remove(ctx context.Context, pkgs []string, opts *manager.Opts) error {
return i.mgr.Remove(opts, pkgs...)
}
func (i *Installer) RemoveAlreadyInstalled(ctx context.Context, pkgs []string) ([]string, error) {
filteredPackages := []string{}
for _, dep := range pkgs {

144
internal/build/plugins.go Normal file
View File

@ -0,0 +1,144 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package build
import (
"fmt"
"log/slog"
"os"
"os/exec"
"strings"
"sync"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-plugin"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
)
var pluginMap = map[string]plugin.Plugin{
"script-executor": &ScriptExecutorPlugin{},
"installer": &InstallerExecutorPlugin{},
"repos": &ReposExecutorPlugin{},
}
var HandshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "ALR_PLUGIN",
MagicCookieValue: "-",
}
func setCommonCmdEnv(cmd *exec.Cmd) {
cmd.Env = []string{
"HOME=/var/cache/alr",
"LOGNAME=alr",
"USER=alr",
"PATH=/usr/bin:/bin:/usr/local/bin",
}
for _, env := range os.Environ() {
if strings.HasPrefix(env, "LANG=") ||
strings.HasPrefix(env, "LANGUAGE=") ||
strings.HasPrefix(env, "LC_") ||
strings.HasPrefix(env, "ALR_LOG_LEVEL=") {
cmd.Env = append(cmd.Env, env)
}
}
}
func GetPluginServeCommonConfig() *plugin.ServeConfig {
return &plugin.ServeConfig{
HandshakeConfig: HandshakeConfig,
Logger: hclog.New(&hclog.LoggerOptions{
Name: "plugin",
Output: os.Stderr,
Level: hclog.Trace,
JSONFormat: true,
DisableTime: true,
}),
}
}
func GetSafeInstaller() (InstallerExecutor, func(), error) {
return getSafeExecutor[InstallerExecutor]("_internal-installer", "installer")
}
func GetSafeScriptExecutor() (ScriptExecutor, func(), error) {
return getSafeExecutor[ScriptExecutor]("_internal-safe-script-executor", "script-executor")
}
func GetSafeReposExecutor() (ReposExecutor, func(), error) {
return getSafeExecutor[ReposExecutor]("_internal-repos", "repos")
}
func getSafeExecutor[T any](subCommand, pluginName string) (T, func(), error) {
var err error
executable, err := os.Executable()
if err != nil {
var zero T
return zero, nil, err
}
cmd := exec.Command(executable, subCommand)
setCommonCmdEnv(cmd)
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 {
var zero T
return zero, nil, err
}
var cleanupOnce sync.Once
cleanup := func() {
cleanupOnce.Do(func() {
client.Kill()
})
}
defer func() {
if err != nil {
slog.Debug("close executor")
cleanup()
}
}()
raw, err := rpcClient.Dispense(pluginName)
if err != nil {
var zero T
return zero, nil, err
}
executor, ok := raw.(T)
if !ok {
var zero T
err = fmt.Errorf("dispensed object is not a %T (got %T)", zero, raw)
return zero, nil, err
}
return executor, cleanup, nil
}

View File

@ -0,0 +1,60 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package build
import (
"context"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
)
//go:generate go run ../../generators/plugin-generator InstallerExecutor ScriptExecutor ReposExecutor
// The Executors interfaces must use context.Context as the first parameter,
// because the plugin-generator cannot generate code without it.
type InstallerExecutor interface {
InstallLocal(ctx context.Context, paths []string, opts *manager.Opts) error
Install(ctx context.Context, pkgs []string, opts *manager.Opts) error
Remove(ctx context.Context, pkgs []string, opts *manager.Opts) error
RemoveAlreadyInstalled(ctx context.Context, pkgs []string) ([]string, error)
}
type ScriptExecutor interface {
ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error)
ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error)
PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error
ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *alrsh.ScriptFile,
varsOfPackages []*alrsh.Package,
repoDeps []string,
builtDeps []*BuiltDep,
basePkg string,
) ([]*BuiltDep, error)
}
type ReposExecutor interface {
PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) (types.Repo, error)
}

View File

@ -0,0 +1,369 @@
// DO NOT EDIT MANUALLY. This file is generated.
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package build
import (
"net/rpc"
"context"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
"github.com/hashicorp/go-plugin"
)
type InstallerExecutorPlugin struct {
Impl InstallerExecutor
}
type InstallerExecutorRPCServer struct {
Impl InstallerExecutor
}
type InstallerExecutorRPC struct {
client *rpc.Client
}
func (p *InstallerExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &InstallerExecutorRPC{client: c}, nil
}
func (p *InstallerExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &InstallerExecutorRPCServer{Impl: p.Impl}, nil
}
type ScriptExecutorPlugin struct {
Impl ScriptExecutor
}
type ScriptExecutorRPCServer struct {
Impl ScriptExecutor
}
type ScriptExecutorRPC struct {
client *rpc.Client
}
func (p *ScriptExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &ScriptExecutorRPC{client: c}, nil
}
func (p *ScriptExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &ScriptExecutorRPCServer{Impl: p.Impl}, nil
}
type ReposExecutorPlugin struct {
Impl ReposExecutor
}
type ReposExecutorRPCServer struct {
Impl ReposExecutor
}
type ReposExecutorRPC struct {
client *rpc.Client
}
func (p *ReposExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &ReposExecutorRPC{client: c}, nil
}
func (p *ReposExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &ReposExecutorRPCServer{Impl: p.Impl}, nil
}
type InstallerExecutorInstallLocalArgs struct {
Paths []string
Opts *manager.Opts
}
type InstallerExecutorInstallLocalResp struct {
}
func (s *InstallerExecutorRPC) InstallLocal(ctx context.Context, paths []string, opts *manager.Opts) error {
var resp *InstallerExecutorInstallLocalResp
err := s.client.Call("Plugin.InstallLocal", &InstallerExecutorInstallLocalArgs{
Paths: paths,
Opts: opts,
}, &resp)
if err != nil {
return err
}
return nil
}
func (s *InstallerExecutorRPCServer) InstallLocal(args *InstallerExecutorInstallLocalArgs, resp *InstallerExecutorInstallLocalResp) error {
err := s.Impl.InstallLocal(context.Background(), args.Paths, args.Opts)
if err != nil {
return err
}
*resp = InstallerExecutorInstallLocalResp{}
return nil
}
type InstallerExecutorInstallArgs struct {
Pkgs []string
Opts *manager.Opts
}
type InstallerExecutorInstallResp struct {
}
func (s *InstallerExecutorRPC) Install(ctx context.Context, pkgs []string, opts *manager.Opts) error {
var resp *InstallerExecutorInstallResp
err := s.client.Call("Plugin.Install", &InstallerExecutorInstallArgs{
Pkgs: pkgs,
Opts: opts,
}, &resp)
if err != nil {
return err
}
return nil
}
func (s *InstallerExecutorRPCServer) Install(args *InstallerExecutorInstallArgs, resp *InstallerExecutorInstallResp) error {
err := s.Impl.Install(context.Background(), args.Pkgs, args.Opts)
if err != nil {
return err
}
*resp = InstallerExecutorInstallResp{}
return nil
}
type InstallerExecutorRemoveArgs struct {
Pkgs []string
Opts *manager.Opts
}
type InstallerExecutorRemoveResp struct {
}
func (s *InstallerExecutorRPC) Remove(ctx context.Context, pkgs []string, opts *manager.Opts) error {
var resp *InstallerExecutorRemoveResp
err := s.client.Call("Plugin.Remove", &InstallerExecutorRemoveArgs{
Pkgs: pkgs,
Opts: opts,
}, &resp)
if err != nil {
return err
}
return nil
}
func (s *InstallerExecutorRPCServer) Remove(args *InstallerExecutorRemoveArgs, resp *InstallerExecutorRemoveResp) error {
err := s.Impl.Remove(context.Background(), args.Pkgs, args.Opts)
if err != nil {
return err
}
*resp = InstallerExecutorRemoveResp{}
return nil
}
type InstallerExecutorRemoveAlreadyInstalledArgs struct {
Pkgs []string
}
type InstallerExecutorRemoveAlreadyInstalledResp struct {
Result0 []string
}
func (s *InstallerExecutorRPC) RemoveAlreadyInstalled(ctx context.Context, pkgs []string) ([]string, error) {
var resp *InstallerExecutorRemoveAlreadyInstalledResp
err := s.client.Call("Plugin.RemoveAlreadyInstalled", &InstallerExecutorRemoveAlreadyInstalledArgs{
Pkgs: pkgs,
}, &resp)
if err != nil {
return nil, err
}
return resp.Result0, nil
}
func (s *InstallerExecutorRPCServer) RemoveAlreadyInstalled(args *InstallerExecutorRemoveAlreadyInstalledArgs, resp *InstallerExecutorRemoveAlreadyInstalledResp) error {
result0, err := s.Impl.RemoveAlreadyInstalled(context.Background(), args.Pkgs)
if err != nil {
return err
}
*resp = InstallerExecutorRemoveAlreadyInstalledResp{
Result0: result0,
}
return nil
}
type ScriptExecutorReadScriptArgs struct {
ScriptPath string
}
type ScriptExecutorReadScriptResp struct {
Result0 *alrsh.ScriptFile
}
func (s *ScriptExecutorRPC) ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error) {
var resp *ScriptExecutorReadScriptResp
err := s.client.Call("Plugin.ReadScript", &ScriptExecutorReadScriptArgs{
ScriptPath: scriptPath,
}, &resp)
if err != nil {
return nil, err
}
return resp.Result0, nil
}
func (s *ScriptExecutorRPCServer) ReadScript(args *ScriptExecutorReadScriptArgs, resp *ScriptExecutorReadScriptResp) error {
result0, err := s.Impl.ReadScript(context.Background(), args.ScriptPath)
if err != nil {
return err
}
*resp = ScriptExecutorReadScriptResp{
Result0: result0,
}
return nil
}
type ScriptExecutorExecuteFirstPassArgs struct {
Input *BuildInput
Sf *alrsh.ScriptFile
}
type ScriptExecutorExecuteFirstPassResp struct {
Result0 string
Result1 []*alrsh.Package
}
func (s *ScriptExecutorRPC) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error) {
var resp *ScriptExecutorExecuteFirstPassResp
err := s.client.Call("Plugin.ExecuteFirstPass", &ScriptExecutorExecuteFirstPassArgs{
Input: input,
Sf: sf,
}, &resp)
if err != nil {
return "", nil, err
}
return resp.Result0, resp.Result1, nil
}
func (s *ScriptExecutorRPCServer) ExecuteFirstPass(args *ScriptExecutorExecuteFirstPassArgs, resp *ScriptExecutorExecuteFirstPassResp) error {
result0, result1, err := s.Impl.ExecuteFirstPass(context.Background(), args.Input, args.Sf)
if err != nil {
return err
}
*resp = ScriptExecutorExecuteFirstPassResp{
Result0: result0,
Result1: result1,
}
return nil
}
type ScriptExecutorPrepareDirsArgs struct {
Input *BuildInput
BasePkg string
}
type ScriptExecutorPrepareDirsResp struct {
}
func (s *ScriptExecutorRPC) PrepareDirs(ctx context.Context, input *BuildInput, basePkg string) error {
var resp *ScriptExecutorPrepareDirsResp
err := s.client.Call("Plugin.PrepareDirs", &ScriptExecutorPrepareDirsArgs{
Input: input,
BasePkg: basePkg,
}, &resp)
if err != nil {
return err
}
return nil
}
func (s *ScriptExecutorRPCServer) PrepareDirs(args *ScriptExecutorPrepareDirsArgs, resp *ScriptExecutorPrepareDirsResp) error {
err := s.Impl.PrepareDirs(context.Background(), args.Input, args.BasePkg)
if err != nil {
return err
}
*resp = ScriptExecutorPrepareDirsResp{}
return nil
}
type ScriptExecutorExecuteSecondPassArgs struct {
Input *BuildInput
Sf *alrsh.ScriptFile
VarsOfPackages []*alrsh.Package
RepoDeps []string
BuiltDeps []*BuiltDep
BasePkg string
}
type ScriptExecutorExecuteSecondPassResp struct {
Result0 []*BuiltDep
}
func (s *ScriptExecutorRPC) ExecuteSecondPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile, varsOfPackages []*alrsh.Package, repoDeps []string, builtDeps []*BuiltDep, basePkg string) ([]*BuiltDep, error) {
var resp *ScriptExecutorExecuteSecondPassResp
err := s.client.Call("Plugin.ExecuteSecondPass", &ScriptExecutorExecuteSecondPassArgs{
Input: input,
Sf: sf,
VarsOfPackages: varsOfPackages,
RepoDeps: repoDeps,
BuiltDeps: builtDeps,
BasePkg: basePkg,
}, &resp)
if err != nil {
return nil, err
}
return resp.Result0, nil
}
func (s *ScriptExecutorRPCServer) ExecuteSecondPass(args *ScriptExecutorExecuteSecondPassArgs, resp *ScriptExecutorExecuteSecondPassResp) error {
result0, err := s.Impl.ExecuteSecondPass(context.Background(), args.Input, args.Sf, args.VarsOfPackages, args.RepoDeps, args.BuiltDeps, args.BasePkg)
if err != nil {
return err
}
*resp = ScriptExecutorExecuteSecondPassResp{
Result0: result0,
}
return nil
}
type ReposExecutorPullOneAndUpdateFromConfigArgs struct {
Repo *types.Repo
}
type ReposExecutorPullOneAndUpdateFromConfigResp struct {
Result0 types.Repo
}
func (s *ReposExecutorRPC) PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) (types.Repo, error) {
var resp *ReposExecutorPullOneAndUpdateFromConfigResp
err := s.client.Call("Plugin.PullOneAndUpdateFromConfig", &ReposExecutorPullOneAndUpdateFromConfigArgs{
Repo: repo,
}, &resp)
if err != nil {
return types.Repo{}, err
}
return resp.Result0, nil
}
func (s *ReposExecutorRPCServer) PullOneAndUpdateFromConfig(args *ReposExecutorPullOneAndUpdateFromConfigArgs, resp *ReposExecutorPullOneAndUpdateFromConfigResp) error {
result0, err := s.Impl.PullOneAndUpdateFromConfig(context.Background(), args.Repo)
if err != nil {
return err
}
*resp = ReposExecutorPullOneAndUpdateFromConfigResp{
Result0: result0,
}
return nil
}

View File

@ -17,24 +17,21 @@
package build
import (
"os"
"os/exec"
"strings"
"context"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/repos"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
)
func setCommonCmdEnv(cmd *exec.Cmd) {
cmd.Env = []string{
"HOME=/var/cache/alr",
"LOGNAME=alr",
"USER=alr",
"PATH=/usr/bin:/bin:/usr/local/bin",
}
for _, env := range os.Environ() {
if strings.HasPrefix(env, "LANG=") ||
strings.HasPrefix(env, "LANGUAGE=") ||
strings.HasPrefix(env, "LC_") ||
strings.HasPrefix(env, "ALR_LOG_LEVEL=") {
cmd.Env = append(cmd.Env, env)
}
}
type reposExecutor struct{ r *repos.Repos }
func NewRepos(r *repos.Repos) ReposExecutor {
return &reposExecutor{r}
}
func (r *reposExecutor) PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) (types.Repo, error) {
if err := r.r.PullOneAndUpdateFromConfig(ctx, repo); err != nil {
return *repo, err
}
return *repo, nil
}

View File

@ -1,150 +0,0 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package build
import (
"fmt"
"log/slog"
"net/rpc"
"os"
"os/exec"
"sync"
"syscall"
"github.com/hashicorp/go-plugin"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
)
type InstallerPlugin struct {
Impl InstallerExecutor
}
type InstallerRPC struct {
client *rpc.Client
}
type InstallerRPCServer struct {
Impl InstallerExecutor
}
type InstallArgs struct {
PackagesOrPaths []string
Opts *manager.Opts
}
func (r *InstallerRPC) InstallLocal(paths []string, opts *manager.Opts) error {
return r.client.Call("Plugin.InstallLocal", &InstallArgs{
PackagesOrPaths: paths,
Opts: opts,
}, nil)
}
func (s *InstallerRPCServer) InstallLocal(args *InstallArgs, reply *struct{}) error {
return s.Impl.InstallLocal(args.PackagesOrPaths, args.Opts)
}
func (r *InstallerRPC) Install(pkgs []string, opts *manager.Opts) error {
return r.client.Call("Plugin.Install", &InstallArgs{
PackagesOrPaths: pkgs,
Opts: opts,
}, nil)
}
func (s *InstallerRPCServer) Install(args *InstallArgs, reply *struct{}) error {
return s.Impl.Install(args.PackagesOrPaths, args.Opts)
}
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")
setCommonCmdEnv(cmd)
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
}

View File

@ -1,273 +0,0 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package build
import (
"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/pkg/alrsh"
)
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) (*alrsh.ScriptFile, error) {
var resp *alrsh.ScriptFile
err := s.client.Call("Plugin.ReadScript", scriptPath, &resp)
return resp, err
}
func (s *ScriptExecutorRPCServer) ReadScript(scriptPath string, resp *alrsh.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 *alrsh.ScriptFile
}
type ExecuteFirstPassResp struct {
BasePkg string
VarsOfPackages []*alrsh.Package
}
func (s *ScriptExecutorRPC) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, 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 *alrsh.ScriptFile
VarsOfPackages []*alrsh.Package
RepoDeps []string
BuiltDeps []*BuiltDep
BasePkg string
}
func (s *ScriptExecutorRPC) ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *alrsh.ScriptFile,
varsOfPackages []*alrsh.Package,
repoDeps []string,
builtDeps []*BuiltDep,
basePkg string,
) ([]*BuiltDep, error) {
var resp []*BuiltDep
err := s.client.Call("Plugin.ExecuteSecondPass", &ExecuteSecondPassArgs{
Input: input,
Sf: sf,
VarsOfPackages: varsOfPackages,
RepoDeps: repoDeps,
BuiltDeps: builtDeps,
BasePkg: basePkg,
}, &resp)
if err != nil {
return nil, err
}
return resp, nil
}
func (s *ScriptExecutorRPCServer) ExecuteSecondPass(args *ExecuteSecondPassArgs, resp *[]*BuiltDep) error {
res, err := s.Impl.ExecuteSecondPass(
context.Background(),
args.Input,
args.Sf,
args.VarsOfPackages,
args.RepoDeps,
args.BuiltDeps,
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")
setCommonCmdEnv(cmd)
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 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
}

View File

@ -23,8 +23,8 @@ import (
"os"
"strings"
"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/pkg/dl"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
)
type SourceDownloader struct {
@ -74,7 +74,7 @@ func (s *SourceDownloader) DownloadSources(
}
}
opts.DlCache = dlcache.New(s.cfg)
opts.DlCache = dlcache.New(s.cfg.GetPaths().CacheDir)
err := dl.Download(ctx, opts)
if err != nil {

View File

@ -20,13 +20,12 @@
package config
import (
"log/slog"
"os"
"fmt"
"path/filepath"
"reflect"
"github.com/caarlos0/env"
"github.com/pelletier/go-toml/v2"
"github.com/goccy/go-yaml"
"github.com/knadh/koanf/providers/confmap"
"github.com/knadh/koanf/v2"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
@ -35,74 +34,65 @@ import (
type ALRConfig struct {
cfg *types.Config
paths *Paths
}
var defaultConfig = &types.Config{
RootCmd: "sudo",
UseRootCmd: true,
PagerStyle: "native",
IgnorePkgUpdates: []string{},
AutoPull: true,
Repos: []types.Repo{},
System *SystemConfig
env *EnvConfig
}
func New() *ALRConfig {
return &ALRConfig{}
return &ALRConfig{
System: NewSystemConfig(),
env: NewEnvConfig(),
}
}
func readConfig(path string) (*types.Config, error) {
file, err := os.Open(path)
if err != nil {
return nil, err
func defaultConfigKoanf() *koanf.Koanf {
k := koanf.New(".")
defaults := map[string]interface{}{
"rootCmd": "sudo",
"useRootCmd": true,
"pagerStyle": "native",
"ignorePkgUpdates": []string{},
"logLevel": "info",
"autoPull": true,
"repos": []types.Repo{},
}
defer file.Close()
config := types.Config{}
if err := toml.NewDecoder(file).Decode(&config); err != nil {
return nil, err
}
return &config, nil
}
func mergeStructs(dst, src interface{}) {
srcVal := reflect.ValueOf(src)
if srcVal.IsNil() {
return
}
srcVal = srcVal.Elem()
dstVal := reflect.ValueOf(dst).Elem()
for i := range srcVal.NumField() {
srcField := srcVal.Field(i)
srcFieldName := srcVal.Type().Field(i).Name
dstField := dstVal.FieldByName(srcFieldName)
if dstField.IsValid() && dstField.CanSet() {
dstField.Set(srcField)
}
if err := k.Load(confmap.Provider(defaults, "."), nil); err != nil {
panic(k)
}
return k
}
func (c *ALRConfig) Load() error {
systemConfig, err := readConfig(
constants.SystemConfigPath,
)
if err != nil {
slog.Debug("Cannot read system config", "err", err)
config := types.Config{}
merged := koanf.New(".")
if err := c.System.Load(); err != nil {
return fmt.Errorf("failed to load system config: %w", err)
}
config := &types.Config{}
mergeStructs(config, defaultConfig)
mergeStructs(config, systemConfig)
err = env.Parse(config)
if err != nil {
return err
if err := c.env.Load(); err != nil {
return fmt.Errorf("failed to load env config: %w", err)
}
c.cfg = config
systemK := c.System.koanf()
envK := c.env.koanf()
if err := merged.Merge(defaultConfigKoanf()); err != nil {
return fmt.Errorf("failed to merge default config: %w", err)
}
if err := merged.Merge(systemK); err != nil {
return fmt.Errorf("failed to merge system config: %w", err)
}
if err := merged.Merge(envK); err != nil {
return fmt.Errorf("failed to merge env config: %w", err)
}
if err := merged.Unmarshal("", &config); err != nil {
return fmt.Errorf("failed to unmarshal merged config: %w", err)
}
c.cfg = &config
c.paths = &Paths{}
c.paths.UserConfigPath = constants.SystemConfigPath
@ -110,52 +100,24 @@ func (c *ALRConfig) Load() error {
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()
return nil
}
func (c *ALRConfig) RootCmd() string {
return c.cfg.RootCmd
}
func (c *ALRConfig) PagerStyle() string {
return c.cfg.PagerStyle
}
func (c *ALRConfig) AutoPull() bool {
return c.cfg.AutoPull
}
func (c *ALRConfig) Repos() []types.Repo {
return c.cfg.Repos
}
func (c *ALRConfig) SetRepos(repos []types.Repo) {
c.cfg.Repos = repos
}
func (c *ALRConfig) IgnorePkgUpdates() []string {
return c.cfg.IgnorePkgUpdates
}
func (c *ALRConfig) LogLevel() string {
return c.cfg.LogLevel
}
func (c *ALRConfig) UseRootCmd() bool {
return c.cfg.UseRootCmd
}
func (c *ALRConfig) GetPaths() *Paths {
return c.paths
}
func (c *ALRConfig) SaveUserConfig() error {
f, err := os.Create(c.paths.UserConfigPath)
func (c *ALRConfig) ToYAML() (string, error) {
data, err := yaml.Marshal(c.cfg)
if err != nil {
return err
return "", err
}
return toml.NewEncoder(f).Encode(c.cfg)
return string(data), nil
}
func (c *ALRConfig) RootCmd() string { return c.cfg.RootCmd }
func (c *ALRConfig) PagerStyle() string { return c.cfg.PagerStyle }
func (c *ALRConfig) AutoPull() bool { return c.cfg.AutoPull }
func (c *ALRConfig) Repos() []types.Repo { return c.cfg.Repos }
func (c *ALRConfig) SetRepos(repos []types.Repo) { c.System.SetRepos(repos) }
func (c *ALRConfig) IgnorePkgUpdates() []string { return c.cfg.IgnorePkgUpdates }
func (c *ALRConfig) LogLevel() string { return c.cfg.LogLevel }
func (c *ALRConfig) UseRootCmd() bool { return c.cfg.UseRootCmd }
func (c *ALRConfig) GetPaths() *Paths { return c.paths }

View File

@ -0,0 +1,76 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package config
import (
"strings"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/v2"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
type EnvConfig struct {
k *koanf.Koanf
}
func NewEnvConfig() *EnvConfig {
return &EnvConfig{
k: koanf.New("."),
}
}
func (c *EnvConfig) koanf() *koanf.Koanf {
return c.k
}
func (c *EnvConfig) Load() error {
allowedKeys := map[string]struct{}{
"ALR_LOG_LEVEL": {},
"ALR_PAGER_STYLE": {},
"ALR_AUTO_PULL": {},
}
err := c.k.Load(env.Provider("ALR_", ".", func(s string) string {
_, ok := allowedKeys[s]
if !ok {
return ""
}
withoutPrefix := strings.TrimPrefix(s, "ALR_")
lowered := strings.ToLower(withoutPrefix)
dotted := strings.ReplaceAll(lowered, "__", ".")
parts := strings.Split(dotted, ".")
for i, part := range parts {
if strings.Contains(part, "_") {
parts[i] = toCamelCase(part)
}
}
return strings.Join(parts, ".")
}), nil)
return err
}
func toCamelCase(s string) string {
parts := strings.Split(s, "_")
for i := 1; i < len(parts); i++ {
if len(parts[i]) > 0 {
parts[i] = cases.Title(language.Und, cases.NoLower).String(parts[i])
}
}
return strings.Join(parts, "")
}

View File

@ -21,9 +21,10 @@ package config
// Paths contains various paths used by ALR
type Paths struct {
UserConfigPath string
CacheDir string
RepoDir string
PkgsDir string
DBPath string
SystemConfigPath string
UserConfigPath string
CacheDir string
RepoDir string
PkgsDir string
DBPath string
}

View File

@ -0,0 +1,144 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package config
import (
"encoding/json"
"errors"
"fmt"
"os"
ktoml "github.com/knadh/koanf/parsers/toml/v2"
"github.com/knadh/koanf/providers/file"
"github.com/knadh/koanf/v2"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/constants"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
)
type SystemConfig struct {
k *koanf.Koanf
cfg *types.Config
}
func NewSystemConfig() *SystemConfig {
return &SystemConfig{
k: koanf.New("."),
cfg: &types.Config{},
}
}
func (c *SystemConfig) koanf() *koanf.Koanf {
return c.k
}
func (c *SystemConfig) Load() error {
if _, err := os.Stat(constants.SystemConfigPath); errors.Is(err, os.ErrNotExist) {
return nil
}
if err := c.k.Load(file.Provider(constants.SystemConfigPath), ktoml.Parser()); err != nil {
return err
}
return c.k.Unmarshal("", c.cfg)
}
func (c *SystemConfig) Save() error {
bytes, err := c.k.Marshal(ktoml.Parser())
if err != nil {
return fmt.Errorf("failed to marshal config: %w", err)
}
file, err := os.Create(constants.SystemConfigPath)
if err != nil {
return fmt.Errorf("failed to create config file: %w", err)
}
defer func() {
if cerr := file.Close(); cerr != nil && err == nil {
err = cerr
}
}()
if _, err := file.Write(bytes); err != nil {
return fmt.Errorf("failed to write config: %w", err)
}
if err := file.Sync(); err != nil {
return fmt.Errorf("failed to sync config: %w", err)
}
return nil
}
func (c *SystemConfig) SetRootCmd(v string) {
err := c.k.Set("rootCmd", v)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetUseRootCmd(v bool) {
err := c.k.Set("useRootCmd", v)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetPagerStyle(v string) {
err := c.k.Set("pagerStyle", v)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetIgnorePkgUpdates(v []string) {
err := c.k.Set("ignorePkgUpdates", v)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetAutoPull(v bool) {
err := c.k.Set("autoPull", v)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetLogLevel(v string) {
err := c.k.Set("logLevel", v)
if err != nil {
panic(err)
}
}
func (c *SystemConfig) SetRepos(v []types.Repo) {
b, err := json.Marshal(v)
if err != nil {
panic(err)
}
var m []interface{}
err = json.Unmarshal(b, &m)
if err != nil {
panic(err)
}
err = c.k.Set("repo", m)
if err != nil {
panic(err)
}
}

View File

@ -65,6 +65,8 @@ func (a *HCLoggerAdapter) Log(level hclog.Level, msg string, args ...interface{}
var chLogLevel chLog.Level
if msg == "plugin process exited" ||
strings.HasPrefix(msg, "[ERR] plugin: stream copy 'stderr' error") ||
strings.HasPrefix(msg, "[WARN] error closing client during Kill") ||
strings.HasPrefix(msg, "[WARN] plugin failed to exit gracefully") ||
strings.HasPrefix(msg, "[DEBUG] plugin") {
chLogLevel = chLog.DebugLevel
} else {

View File

@ -22,6 +22,7 @@ package overrides
import (
"fmt"
"regexp"
"strconv"
"strings"
"golang.org/x/exp/slices"
@ -182,3 +183,18 @@ func ReleasePlatformSpecific(release int, info *distro.OSRelease) string {
return fmt.Sprintf("%d", release)
}
func ParseReleasePlatformSpecific(s string, info *distro.OSRelease) (int, error) {
if info.ID == "altlinux" {
if strings.HasPrefix(s, "alt") {
return strconv.Atoi(s[3:])
}
}
if info.ID == "fedora" || slices.Contains(info.Like, "fedora") {
parts := strings.SplitN(s, ".", 2)
return strconv.Atoi(parts[0])
}
return strconv.Atoi(s)
}

View File

@ -233,5 +233,8 @@ func TestReleasePlatformSpecific(t *testing.T) {
},
} {
assert.Equal(t, tc.expected, overrides.ReleasePlatformSpecific(1, tc.info))
release, err := overrides.ParseReleasePlatformSpecific(tc.expected, tc.info)
assert.NoError(t, err)
assert.Equal(t, 1, release)
}
}

View File

@ -21,44 +21,58 @@ package repos
import (
"context"
"fmt"
"strings"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
)
func (rs *Repos) FindPkgs(ctx context.Context, pkgs []string) (map[string][]alrsh.Package, []string, error) {
found := map[string][]alrsh.Package{}
notFound := []string(nil)
found := make(map[string][]alrsh.Package)
var notFound []string
for _, pkgName := range pkgs {
if pkgName == "" {
continue
}
result, err := rs.db.GetPkgs(ctx, "json_array_contains(provides, ?)", pkgName)
if err != nil {
return nil, nil, err
}
var result []alrsh.Package
var err error
added := 0
for _, pkg := range result {
added++
found[pkgName] = append(found[pkgName], pkg)
}
switch {
case strings.Contains(pkgName, "/"):
// repo/pkg
parts := strings.SplitN(pkgName, "/", 2)
repo := parts[0]
name := parts[1]
result, err = rs.db.GetPkgs(ctx, "name = ? AND repository = ?", name, repo)
if added == 0 {
result, err := rs.db.GetPkgs(ctx, "name LIKE ?", pkgName)
case strings.Contains(pkgName, "+alr-"):
// pkg+alr-repo
parts := strings.SplitN(pkgName, "+alr-", 2)
name := parts[0]
repo := parts[1]
result, err = rs.db.GetPkgs(ctx, "name = ? AND repository = ?", name, repo)
default:
result, err = rs.db.GetPkgs(ctx, "json_array_contains(provides, ?)", pkgName)
if err != nil {
return nil, nil, err
return nil, nil, fmt.Errorf("FindPkgs: get by provides: %w", err)
}
for _, pkg := range result {
added++
found[pkgName] = append(found[pkgName], pkg)
if len(result) == 0 {
result, err = rs.db.GetPkgs(ctx, "name LIKE ?", pkgName)
}
}
if added == 0 {
if err != nil {
return nil, nil, fmt.Errorf("FindPkgs: lookup for %q failed: %w", pkgName, err)
}
if len(result) == 0 {
notFound = append(notFound, pkgName)
} else {
found[pkgName] = result
}
}

View File

@ -31,7 +31,6 @@ import (
"strings"
"github.com/go-git/go-billy/v5"
"github.com/go-git/go-billy/v5/osfs"
"github.com/go-git/go-git/v5"
gitConfig "github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
@ -69,159 +68,240 @@ func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error {
}
for _, repo := range repos {
repoURL, err := url.Parse(repo.URL)
err := rs.pullRepo(ctx, &repo, false)
if err != nil {
return err
}
}
slog.Info(gotext.Get("Pulling repository"), "name", repo.Name)
repoDir := filepath.Join(rs.cfg.GetPaths().RepoDir, repo.Name)
return nil
}
var repoFS billy.Filesystem
gitDir := filepath.Join(repoDir, ".git")
// Only pull repos that contain valid git repos
if fi, err := os.Stat(gitDir); err == nil && fi.IsDir() {
r, err := git.PlainOpen(repoDir)
if err != nil {
return err
}
func (rs *Repos) PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) error {
err := rs.pullRepo(ctx, repo, true)
if err != nil {
return err
}
err = r.FetchContext(ctx, &git.FetchOptions{
Progress: os.Stderr,
Force: true,
})
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return err
}
return nil
}
w, err := r.Worktree()
if err != nil {
return err
}
func (rs *Repos) pullRepo(ctx context.Context, repo *types.Repo, updateRepoFromToml bool) error {
urls := []string{repo.URL}
urls = append(urls, repo.Mirrors...)
old, err := r.Head()
if err != nil {
return err
}
var lastErr error
revHash, err := resolveHash(r, repo.Ref)
if err != nil {
return fmt.Errorf("error resolving hash: %w", err)
}
if old.Hash() == *revHash {
slog.Info(gotext.Get("Repository up to date"), "name", repo.Name)
}
err = w.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(revHash.String()),
Force: true,
})
if err != nil {
return err
}
repoFS = w.Filesystem
new, err := r.Head()
if err != nil {
return err
}
// If the DB was not present at startup, that means it's
// empty. In this case, we need to update the DB fully
// rather than just incrementally.
if rs.db.IsEmpty() {
err = rs.processRepoFull(ctx, repo, repoDir)
if err != nil {
return err
}
} else {
err = rs.processRepoChanges(ctx, repo, r, w, old, new)
if err != nil {
return err
}
}
} else {
err = os.RemoveAll(repoDir)
if err != nil {
return err
}
err = os.MkdirAll(repoDir, 0o755)
if err != nil {
return err
}
r, err := git.PlainInit(repoDir, false)
if err != nil {
return err
}
_, err = r.CreateRemote(&gitConfig.RemoteConfig{
Name: git.DefaultRemoteName,
URLs: []string{repoURL.String()},
})
if err != nil {
return err
}
err = r.FetchContext(ctx, &git.FetchOptions{
Progress: os.Stderr,
Force: true,
})
if err != nil {
return err
}
w, err := r.Worktree()
if err != nil {
return err
}
revHash, err := resolveHash(r, repo.Ref)
if err != nil {
return fmt.Errorf("error resolving hash: %w", err)
}
err = w.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(revHash.String()),
Force: true,
})
if err != nil {
return err
}
err = rs.processRepoFull(ctx, repo, repoDir)
if err != nil {
return err
}
repoFS = osfs.New(repoDir)
for i, repoURL := range urls {
if i > 0 {
slog.Info(gotext.Get("Trying mirror"), "repo", repo.Name, "mirror", repoURL)
}
fl, err := repoFS.Open("alr-repo.toml")
err := rs.pullRepoFromURL(ctx, repoURL, repo, updateRepoFromToml)
if err != nil {
slog.Warn(gotext.Get("Git repository does not appear to be a valid ALR repo"), "repo", repo.Name)
lastErr = err
slog.Warn(gotext.Get("Failed to pull from URL"), "repo", repo.Name, "url", repoURL, "error", err)
continue
}
var repoCfg types.RepoConfig
err = toml.NewDecoder(fl).Decode(&repoCfg)
// Success
return nil
}
return fmt.Errorf("failed to pull repository %s from any URL: %w", repo.Name, lastErr)
}
func readGitRepo(repoDir, repoUrl string) (*git.Repository, bool, error) {
gitDir := filepath.Join(repoDir, ".git")
if fi, err := os.Stat(gitDir); err == nil && fi.IsDir() {
r, err := git.PlainOpen(repoDir)
if err == nil {
err = updateRemoteURL(r, repoUrl)
if err == nil {
_, err := r.Head()
if err == nil {
return r, false, nil
}
if errors.Is(err, plumbing.ErrReferenceNotFound) {
return r, true, nil
}
slog.Debug("error getting HEAD, reinitializing...", "err", err)
}
}
slog.Debug("error while reading repo, reinitializing...", "err", err)
}
if err := os.RemoveAll(repoDir); err != nil {
return nil, false, fmt.Errorf("failed to remove repo directory: %w", err)
}
if err := os.MkdirAll(repoDir, 0o755); err != nil {
return nil, false, fmt.Errorf("failed to create repo directory: %w", err)
}
r, err := git.PlainInit(repoDir, false)
if err != nil {
return nil, false, fmt.Errorf("failed to initialize git repo: %w", err)
}
_, err = r.CreateRemote(&gitConfig.RemoteConfig{
Name: git.DefaultRemoteName,
URLs: []string{repoUrl},
})
if err != nil {
return nil, false, err
}
return r, true, nil
}
func (rs *Repos) pullRepoFromURL(ctx context.Context, rawRepoUrl string, repo *types.Repo, update bool) error {
repoURL, err := url.Parse(rawRepoUrl)
if err != nil {
return fmt.Errorf("invalid URL %s: %w", rawRepoUrl, err)
}
slog.Info(gotext.Get("Pulling repository"), "name", repo.Name)
repoDir := filepath.Join(rs.cfg.GetPaths().RepoDir, repo.Name)
var repoFS billy.Filesystem
r, freshGit, err := readGitRepo(repoDir, repoURL.String())
if err != nil {
return fmt.Errorf("failed to open repo")
}
err = r.FetchContext(ctx, &git.FetchOptions{
Progress: os.Stderr,
Force: true,
})
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return err
}
var old *plumbing.Reference
w, err := r.Worktree()
if err != nil {
return err
}
revHash, err := resolveHash(r, repo.Ref)
if err != nil {
return fmt.Errorf("error resolving hash: %w", err)
}
if !freshGit {
old, err = r.Head()
if err != nil {
return err
}
fl.Close()
// If the version doesn't have a "v" prefix, it's not a standard version.
// It may be "unknown" or a git version, but either way, there's no way
// to compare it to the repo version, so only compare versions with the "v".
if strings.HasPrefix(config.Version, "v") {
if vercmp.Compare(config.Version, repoCfg.Repo.MinVersion) == -1 {
slog.Warn(gotext.Get("ALR repo's minimum ALR version is greater than the current version. Try updating ALR if something doesn't work."), "repo", repo.Name)
}
if old.Hash() == *revHash {
slog.Info(gotext.Get("Repository up to date"), "name", repo.Name)
}
}
err = w.Checkout(&git.CheckoutOptions{
Hash: plumbing.NewHash(revHash.String()),
Force: true,
})
if err != nil {
return err
}
repoFS = w.Filesystem
new, err := r.Head()
if err != nil {
return err
}
// If the DB was not present at startup, that means it's
// empty. In this case, we need to update the DB fully
// rather than just incrementally.
if rs.db.IsEmpty() || freshGit {
err = rs.processRepoFull(ctx, *repo, repoDir)
if err != nil {
return err
}
} else {
err = rs.processRepoChanges(ctx, *repo, r, w, old, new)
if err != nil {
return err
}
}
fl, err := repoFS.Open("alr-repo.toml")
if err != nil {
slog.Warn(gotext.Get("Git repository does not appear to be a valid ALR repo"), "repo", repo.Name)
return nil
}
var repoCfg types.RepoConfig
err = toml.NewDecoder(fl).Decode(&repoCfg)
if err != nil {
return err
}
fl.Close()
// If the version doesn't have a "v" prefix, it's not a standard version.
// It may be "unknown" or a git version, but either way, there's no way
// to compare it to the repo version, so only compare versions with the "v".
if strings.HasPrefix(config.Version, "v") {
if vercmp.Compare(config.Version, repoCfg.Repo.MinVersion) == -1 {
slog.Warn(gotext.Get("ALR repo's minimum ALR version is greater than the current version. Try updating ALR if something doesn't work."), "repo", repo.Name)
}
}
if update {
if repoCfg.Repo.URL != "" {
repo.URL = repoCfg.Repo.URL
}
if repoCfg.Repo.Ref != "" {
repo.Ref = repoCfg.Repo.Ref
}
if len(repoCfg.Repo.Mirrors) > 0 {
repo.Mirrors = repoCfg.Repo.Mirrors
}
}
return nil
}
func updateRemoteURL(r *git.Repository, newURL string) error {
cfg, err := r.Config()
if err != nil {
return err
}
remote, ok := cfg.Remotes[git.DefaultRemoteName]
if !ok || len(remote.URLs) == 0 {
return fmt.Errorf("no remote '%s' found", git.DefaultRemoteName)
}
currentURL := remote.URLs[0]
if currentURL == newURL {
return nil
}
slog.Debug("Updating remote URL", "old", currentURL, "new", newURL)
err = r.DeleteRemote(git.DefaultRemoteName)
if err != nil {
return fmt.Errorf("failed to delete old remote: %w", err)
}
_, err = r.CreateRemote(&gitConfig.RemoteConfig{
Name: git.DefaultRemoteName,
URLs: []string{newURL},
})
if err != nil {
return fmt.Errorf("failed to create new remote: %w", err)
}
return nil
}

View File

@ -26,7 +26,6 @@ import (
"testing"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/repos"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/types"
@ -35,7 +34,7 @@ import (
type TestEnv struct {
Ctx context.Context
Cfg *TestALRConfig
Db *db.Database
Db *database.Database
}
type TestALRConfig struct {

View File

@ -23,7 +23,6 @@ import (
"context"
"errors"
"fmt"
"log/slog"
"reflect"
"strings"
@ -75,75 +74,99 @@ func New(info *distro.OSRelease, runner *interp.Runner) *Decoder {
// DecodeVar decodes a variable to val using reflection.
// Structs should use the "sh" struct tag.
func (d *Decoder) DecodeVar(name string, val any) error {
variable := d.getVar(name)
if variable == nil {
return VarNotFoundError{name}
}
origType := reflect.TypeOf(val).Elem()
isOverridableField := strings.Contains(origType.String(), "OverridableField[")
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
WeaklyTypedInput: true,
DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) {
if strings.Contains(to.Type().String(), "alrsh.OverridableField") {
if to.Kind() != reflect.Ptr && to.CanAddr() {
to = to.Addr()
}
if !isOverridableField {
variable := d.getVarNoOverrides(name)
if variable == nil {
return VarNotFoundError{name}
}
names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name))
if err != nil {
return nil, err
}
isNotSet := true
setMethod := to.MethodByName("Set")
setResolvedMethod := to.MethodByName("SetResolved")
for _, varName := range names {
val := d.getVarNoOverrides(varName)
if val == nil {
continue
dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
WeaklyTypedInput: true,
Result: val, // передаем указатель на новое значение
TagName: "sh",
DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) {
if from.Kind() == reflect.Slice && to.Kind() == reflect.String {
s, ok := from.Interface().([]string)
if ok && len(s) == 1 {
return s[0], nil
}
t := setMethod.Type().In(1)
newVal := from
if !newVal.Type().AssignableTo(t) {
newVal = reflect.New(t)
err = d.DecodeVar(name, newVal.Interface())
if err != nil {
return nil, err
}
newVal = newVal.Elem()
}
if isNotSet {
setResolvedMethod.Call([]reflect.Value{newVal})
}
override := strings.TrimPrefix(strings.TrimPrefix(varName, name), "_")
setMethod.Call([]reflect.Value{reflect.ValueOf(override), newVal})
}
return from.Interface(), nil
}),
})
if err != nil {
return err
}
return to, nil
switch variable.Kind {
case expand.Indexed:
return dec.Decode(variable.List)
case expand.Associative:
return dec.Decode(variable.Map)
default:
return dec.Decode(variable.Str)
}
} else {
vars := d.getVarsByPrefix(name)
if len(vars) == 0 {
return VarNotFoundError{name}
}
reflectVal := reflect.ValueOf(val)
overridableVal := reflect.ValueOf(val).Elem()
dataField := overridableVal.FieldByName("data")
if !dataField.IsValid() {
return fmt.Errorf("data field not found in OverridableField")
}
mapType := dataField.Type() // map[string]T
elemType := mapType.Elem() // T
var overridablePtr reflect.Value
if reflectVal.Kind() == reflect.Ptr {
overridablePtr = reflectVal
} else {
if !reflectVal.CanAddr() {
return fmt.Errorf("OverridableField value is not addressable")
}
return from.Interface(), nil
}),
Result: val,
TagName: "sh",
})
if err != nil {
slog.Warn("err", "err", err)
return err
}
overridablePtr = reflectVal.Addr()
}
switch variable.Kind {
case expand.Indexed:
return dec.Decode(variable.List)
case expand.Associative:
return dec.Decode(variable.Map)
default:
return dec.Decode(variable.Str)
setValue := overridablePtr.MethodByName("Set")
if !setValue.IsValid() {
return fmt.Errorf("method Set not found on OverridableField")
}
for _, v := range vars {
varName := v.Name
key := strings.TrimPrefix(strings.TrimPrefix(varName, name), "_")
newVal := reflect.New(elemType)
if err := d.DecodeVar(varName, newVal.Interface()); err != nil {
return err
}
keyValue := reflect.ValueOf(key)
setValue.Call([]reflect.Value{keyValue, newVal.Elem()})
}
resolveValue := overridablePtr.MethodByName("Resolve")
if !resolveValue.IsValid() {
return fmt.Errorf("method Resolve not found on OverridableField")
}
names, err := overrides.Resolve(d.info, overrides.DefaultOpts)
if err != nil {
return err
}
resolveValue.Call([]reflect.Value{reflect.ValueOf(names)})
return nil
}
}
@ -284,23 +307,6 @@ func (d *Decoder) getFunc(name string) *syntax.Stmt {
return nil
}
// getVar gets a variable based on its name, taking into account
// override variables and nameref variables.
func (d *Decoder) getVar(name string) *expand.Variable {
names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name))
if err != nil {
return nil
}
for _, varName := range names {
res := d.getVarNoOverrides(varName)
if res != nil {
return res
}
}
return nil
}
func (d *Decoder) getVarNoOverrides(name string) *expand.Variable {
val, ok := d.Runner.Vars[name]
if ok {
@ -318,6 +324,32 @@ func (d *Decoder) getVarNoOverrides(name string) *expand.Variable {
return nil
}
type vars struct {
Name string
Value *expand.Variable
}
func (d *Decoder) getVarsByPrefix(prefix string) []*vars {
result := make([]*vars, 0)
for name, val := range d.Runner.Vars {
if !strings.HasPrefix(name, prefix) {
continue
}
switch prefix {
case "auto_req":
if strings.HasPrefix(name, "auto_req_skiplist") {
continue
}
case "auto_prov":
if strings.HasPrefix(name, "auto_prov_skiplist") {
continue
}
}
result = append(result, &vars{name, &val})
}
return result
}
func IsTruthy(value string) bool {
value = strings.ToLower(strings.TrimSpace(value))
return value == "true" || value == "yes" || value == "1"

View File

@ -32,24 +32,25 @@ import (
"mvdan.cc/sh/v3/syntax"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
)
type BuildVars struct {
Name string `sh:"name,required"`
Version string `sh:"version,required"`
Release int `sh:"release,required"`
Epoch uint `sh:"epoch"`
Description string `sh:"desc"`
Homepage string `sh:"homepage"`
Maintainer string `sh:"maintainer"`
Architectures []string `sh:"architectures"`
Licenses []string `sh:"license"`
Provides []string `sh:"provides"`
Conflicts []string `sh:"conflicts"`
Depends []string `sh:"deps"`
BuildDepends []string `sh:"build_deps"`
Replaces []string `sh:"replaces"`
Name string `sh:"name,required"`
Version string `sh:"version,required"`
Release int `sh:"release,required"`
Epoch uint `sh:"epoch"`
Description alrsh.OverridableField[string] `sh:"desc"`
Homepage string `sh:"homepage"`
Maintainer string `sh:"maintainer"`
Architectures []string `sh:"architectures"`
Licenses []string `sh:"license"`
Provides []string `sh:"provides"`
Conflicts []string `sh:"conflicts"`
Depends []string `sh:"deps"`
BuildDepends alrsh.OverridableField[[]string] `sh:"build_deps"`
Replaces alrsh.OverridableField[[]string] `sh:"replaces"`
}
const testScript = `
@ -113,22 +114,34 @@ func TestDecodeVars(t *testing.T) {
}
expected := BuildVars{
Name: "test",
Version: "0.0.1",
Release: 1,
Epoch: 2,
Description: "Test package",
Name: "test",
Version: "0.0.1",
Release: 1,
Epoch: 2,
Description: alrsh.OverridableFromMap(map[string]string{
"": "Test package",
}),
Homepage: "https://gitea.plemya-x.ru/xpamych/ALR",
Maintainer: "Евгений Храмов <xpamych@yandex.ru>",
Architectures: []string{"arm64", "amd64"},
Licenses: []string{"GPL-3.0-or-later"},
Provides: []string{"test"},
Conflicts: []string{"test"},
Replaces: []string{"test-legacy"},
Depends: []string{"sudo"},
BuildDepends: []string{"go"},
Replaces: alrsh.OverridableFromMap(map[string][]string{
"": {"test-old"},
"test_os": {"test-legacy"},
}),
Depends: []string{"sudo"},
BuildDepends: alrsh.OverridableFromMap(map[string][]string{
"": {"golang"},
"arch": {"go"},
}),
}
expected.Description.SetResolved("Test package")
expected.Replaces.SetResolved([]string{"test-legacy"})
expected.BuildDepends.SetResolved([]string{"go"})
if !reflect.DeepEqual(bv, expected) {
t.Errorf("Expected %v, got %v", expected, bv)
}

View File

@ -0,0 +1,53 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package helpers
import (
"io/fs"
"os"
"path/filepath"
)
// dirLfs implements fs.FS like os.DirFS but uses LStat instead of Stat.
// This means symbolic links are treated as links themselves rather than
// being followed to their targets.
type dirLfs struct {
fs.FS
dir string
}
func NewDirLFS(dir string) *dirLfs {
return &dirLfs{
FS: os.DirFS(dir),
dir: dir,
}
}
func (d *dirLfs) Stat(name string) (fs.FileInfo, error) {
if !fs.ValidPath(name) {
return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrInvalid}
}
fullPath := filepath.Join(d.dir, filepath.FromSlash(name))
info, err := os.Lstat(fullPath)
if err != nil {
return nil, &fs.PathError{Op: "stat", Path: name, Err: err}
}
return info, nil
}

View File

@ -0,0 +1,179 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package helpers
import (
"fmt"
"os"
"path"
"path/filepath"
"github.com/bmatcuk/doublestar/v4"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
)
func matchNamePattern(name, pattern string) bool {
matched, err := filepath.Match(pattern, name)
if err != nil {
return false
}
return matched
}
func validateDir(dirPath, commandName string) error {
info, err := os.Stat(dirPath)
if err != nil {
return fmt.Errorf("%s: %w", commandName, err)
}
if !info.IsDir() {
return fmt.Errorf("%s: %s is not a directory", commandName, dirPath)
}
return nil
}
func outputFiles(hc interp.HandlerContext, files []string) error {
for _, file := range files {
v, err := syntax.Quote(file, syntax.LangAuto)
if err != nil {
return err
}
fmt.Fprintln(hc.Stdout, v)
}
return nil
}
func makeRelativePath(basePath, fullPath string) (string, error) {
relPath, err := filepath.Rel(basePath, fullPath)
if err != nil {
return "", err
}
return "./" + relPath, nil
}
func filesFindLangCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*.mo"
if len(args) > 0 {
namePattern = args[0] + ".mo"
}
localePath := "./usr/share/locale/"
realPath := path.Join(hc.Dir, localePath)
if err := validateDir(realPath, "files-find-lang"); err != nil {
return err
}
var langFiles []string
err := filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && matchNamePattern(info.Name(), namePattern) {
relPath, relErr := makeRelativePath(hc.Dir, p)
if relErr != nil {
return relErr
}
langFiles = append(langFiles, relPath)
}
return nil
})
if err != nil {
return fmt.Errorf("files-find-lang: %w", err)
}
return outputFiles(hc, langFiles)
}
func filesFindDocCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*"
if len(args) > 0 {
namePattern = args[0]
}
docPath := "./usr/share/doc/"
docRealPath := path.Join(hc.Dir, docPath)
if err := validateDir(docRealPath, "files-find-doc"); err != nil {
return err
}
var docFiles []string
entries, err := os.ReadDir(docRealPath)
if err != nil {
return fmt.Errorf("files-find-doc: %w", err)
}
for _, entry := range entries {
if matchNamePattern(entry.Name(), namePattern) {
targetPath := filepath.Join(docRealPath, entry.Name())
targetInfo, err := os.Stat(targetPath)
if err != nil {
return fmt.Errorf("files-find-doc: %w", err)
}
if targetInfo.IsDir() {
err := filepath.Walk(targetPath, func(subPath string, subInfo os.FileInfo, subErr error) error {
if subErr != nil {
return subErr
}
relPath, err := makeRelativePath(hc.Dir, subPath)
if err != nil {
return err
}
docFiles = append(docFiles, relPath)
return nil
})
if err != nil {
return fmt.Errorf("files-find-doc: %w", err)
}
}
}
}
return outputFiles(hc, docFiles)
}
func filesFindCmd(hc interp.HandlerContext, cmd string, args []string) error {
if len(args) == 0 {
return fmt.Errorf("files-find: at least one glob pattern is required")
}
var foundFiles []string
for _, globPattern := range args {
searchPath := path.Join(hc.Dir, globPattern)
basepath, pattern := doublestar.SplitPattern(searchPath)
fsys := NewDirLFS(basepath)
matches, err := doublestar.Glob(fsys, pattern, doublestar.WithNoFollow(), doublestar.WithFailOnPatternNotExist())
if err != nil {
return fmt.Errorf("files-find: glob pattern error: %w", err)
}
for _, match := range matches {
relPath, err := makeRelativePath(hc.Dir, path.Join(basepath, match))
if err != nil {
continue
}
foundFiles = append(foundFiles, relPath)
}
}
return outputFiles(hc, foundFiles)
}

View File

@ -24,7 +24,6 @@ import (
"fmt"
"io"
"os"
"path"
"path/filepath"
"strconv"
"strings"
@ -57,6 +56,7 @@ var Helpers = handlers.ExecFuncs{
"install-library": installLibraryCmd,
"git-version": gitVersionCmd,
"files-find": filesFindCmd,
"files-find-lang": filesFindLangCmd,
"files-find-doc": filesFindDocCmd,
}
@ -65,6 +65,7 @@ var Helpers = handlers.ExecFuncs{
// that don't modify any state
var Restricted = handlers.ExecFuncs{
"git-version": gitVersionCmd,
"files-find": filesFindCmd,
"files-find-lang": filesFindLangCmd,
"files-find-doc": filesFindDocCmd,
}
@ -265,114 +266,6 @@ func gitVersionCmd(hc interp.HandlerContext, cmd string, args []string) error {
return nil
}
func filesFindLangCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*.mo"
if len(args) > 0 {
namePattern = args[0] + ".mo"
}
localePath := "./usr/share/locale/"
realPath := path.Join(hc.Dir, localePath)
info, err := os.Stat(realPath)
if err != nil {
return fmt.Errorf("files-find-lang: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("files-find-lang: %s is not a directory", localePath)
}
var langFiles []string
err = filepath.Walk(realPath, func(p string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if !info.IsDir() && matchNamePattern(info.Name(), namePattern) {
relPath, relErr := filepath.Rel(hc.Dir, p)
if relErr != nil {
return relErr
}
langFiles = append(langFiles, "./"+relPath)
}
return nil
})
if err != nil {
return fmt.Errorf("files-find-lang: %w", err)
}
for _, file := range langFiles {
fmt.Fprintln(hc.Stdout, file)
}
return nil
}
func filesFindDocCmd(hc interp.HandlerContext, cmd string, args []string) error {
namePattern := "*"
if len(args) > 0 {
namePattern = args[0]
}
docPath := "./usr/share/doc/"
docRealPath := path.Join(hc.Dir, docPath)
info, err := os.Stat(docRealPath)
if err != nil {
return fmt.Errorf("files-find-doc: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("files-find-doc: %s is not a directory", docPath)
}
var docFiles []string
entries, err := os.ReadDir(docRealPath)
if err != nil {
return err
}
for _, entry := range entries {
if matchNamePattern(entry.Name(), namePattern) {
targetPath := filepath.Join(docRealPath, entry.Name())
targetInfo, err := os.Stat(targetPath)
if err != nil {
return err
}
if targetInfo.IsDir() {
err := filepath.Walk(targetPath, func(subPath string, subInfo os.FileInfo, subErr error) error {
relPath, err := filepath.Rel(hc.Dir, subPath)
if err != nil {
return err
}
docFiles = append(docFiles, "./"+relPath)
return nil
})
if err != nil {
return err
}
}
}
}
if err != nil {
return fmt.Errorf("files-find-doc: %w", err)
}
for _, file := range docFiles {
fmt.Fprintln(hc.Stdout, file)
}
return nil
}
func matchNamePattern(name, pattern string) bool {
matched, err := filepath.Match(pattern, name)
if err != nil {
return false
}
return matched
}
func helperInstall(from, to string, perms os.FileMode) error {
err := os.MkdirAll(filepath.Dir(to), 0o755)
if err != nil {

View File

@ -24,6 +24,8 @@ import (
"strings"
"testing"
"github.com/bmatcuk/doublestar/v4"
"github.com/google/shlex"
"github.com/stretchr/testify/assert"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
@ -31,12 +33,19 @@ import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/handlers"
)
type symlink struct {
linkPath string
targetPath string
}
type testCase struct {
name string
dirsToCreate []string
filesToCreate []string
expectedOutput []string
args string
name string
dirsToCreate []string
filesToCreate []string
expectedOutput []string
symlinksToCreate []symlink
args string
expectedError error
}
func TestFindFilesDoc(t *testing.T) {
@ -125,7 +134,8 @@ files-find-doc ` + tc.args
err = runner.Run(context.Background(), script)
assert.NoError(t, err)
contents := strings.Fields(strings.TrimSpace(buf.String()))
contents, err := shlex.Split(buf.String())
assert.NoError(t, err)
assert.ElementsMatch(t, tc.expectedOutput, contents)
})
}
@ -209,7 +219,120 @@ files-find-lang ` + tc.args
err = runner.Run(context.Background(), script)
assert.NoError(t, err)
contents := strings.Fields(strings.TrimSpace(buf.String()))
contents, err := shlex.Split(buf.String())
assert.NoError(t, err)
assert.ElementsMatch(t, tc.expectedOutput, contents)
})
}
}
func TestFindFiles(t *testing.T) {
tests := []testCase{
{
name: "With file and dir symlinks",
dirsToCreate: []string{
"usr/share/locale/ru/LC_MESSAGES",
"usr/share/locale/tr/LC_MESSAGES",
"opt/app",
"opt/app/internal",
"opt/app/with space",
"usr/bin",
},
filesToCreate: []string{
"usr/share/locale/ru/LC_MESSAGES/yandex-disk.mo",
"usr/share/locale/ru/LC_MESSAGES/yandex-disk-indicator.mo",
"usr/share/locale/tr/LC_MESSAGES/yandex-disk.mo",
"opt/app/internal/test",
"opt/app/with space/file",
},
symlinksToCreate: []symlink{
{
linkPath: "/opt/app/etc",
targetPath: "/etc",
},
{
linkPath: "/usr/bin/file",
targetPath: "/not-existing",
},
},
expectedOutput: []string{
"./usr/share/locale/ru/LC_MESSAGES/yandex-disk.mo",
"./usr/share/locale/ru/LC_MESSAGES/yandex-disk-indicator.mo",
"./usr/share/locale/tr/LC_MESSAGES/yandex-disk.mo",
"./opt/app/etc",
"./opt/app/internal",
"./opt/app/internal/test",
"./opt/app/with space",
"./opt/app/with space/file",
"./usr/bin/file",
},
args: "\"/usr/share/locale/*/LC_MESSAGES/*.mo\" \"/opt/app/**/*\" \"/usr/bin/file\"",
expectedError: nil,
},
{
name: "Not existing paths should throw error",
args: "\"/opt/test/not-existing\"",
expectedError: doublestar.ErrPatternNotExist,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
tempDir, err := os.MkdirTemp("", "test-files-find")
assert.NoError(t, err)
defer os.RemoveAll(tempDir)
for _, dir := range tc.dirsToCreate {
dirPath := filepath.Join(tempDir, dir)
err := os.MkdirAll(dirPath, 0o755)
assert.NoError(t, err)
}
for _, file := range tc.filesToCreate {
filePath := filepath.Join(tempDir, file)
err := os.WriteFile(filePath, []byte("test content"), 0o644)
assert.NoError(t, err)
}
for _, sl := range tc.symlinksToCreate {
linkFullPath := filepath.Join(tempDir, sl.linkPath)
targetFullPath := sl.targetPath
// make sure parent dir exists
err := os.MkdirAll(filepath.Dir(linkFullPath), 0o755)
assert.NoError(t, err)
err = os.Symlink(targetFullPath, linkFullPath)
assert.NoError(t, err)
}
helpers := handlers.ExecFuncs{
"files-find": filesFindCmd,
}
buf := &bytes.Buffer{}
runner, err := interp.New(
interp.Dir(tempDir),
interp.StdIO(os.Stdin, buf, os.Stderr),
interp.ExecHandler(helpers.ExecHandler(interp.DefaultExecHandler(1000))),
)
assert.NoError(t, err)
scriptContent := `
shopt -s globstar
files-find ` + tc.args
script, err := syntax.NewParser().Parse(strings.NewReader(scriptContent), "")
assert.NoError(t, err)
err = runner.Run(context.Background(), script)
if tc.expectedError != nil {
assert.ErrorAs(t, err, &tc.expectedError)
} else {
assert.NoError(t, err)
}
contents, err := shlex.Split(buf.String())
assert.NoError(t, err)
assert.ElementsMatch(t, tc.expectedOutput, contents)
})
}

View File

@ -9,55 +9,99 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: build.go:42
#: build.go:41
msgid "Build a local package"
msgstr ""
#: build.go:48
#: build.go:47
msgid "Path to the build script"
msgstr ""
#: build.go:53
#: build.go:52
msgid "Specify subpackage in script (for multi package script only)"
msgstr ""
#: build.go:58
#: build.go:57
msgid "Name of the package to build and its repo (example: default/go-bin)"
msgstr ""
#: build.go:63
#: build.go:62
msgid ""
"Build package from scratch even if there's an already built package available"
msgstr ""
#: build.go:73
#: build.go:72
msgid "Error getting working directory"
msgstr ""
#: build.go:118
#: build.go:117
msgid "Cannot get absolute script path"
msgstr ""
#: build.go:152
#: build.go:143
msgid "Package not found"
msgstr ""
#: build.go:165
#: build.go:156
msgid "Nothing to build"
msgstr ""
#: build.go:222
#: build.go:213
msgid "Error building package"
msgstr ""
#: build.go:229
#: build.go:220
msgid "Error moving the package"
msgstr ""
#: build.go:233
#: build.go:224
msgid "Done"
msgstr ""
#: config.go:36
msgid "Manage config"
msgstr ""
#: config.go:48
msgid "Show config"
msgstr ""
#: config.go:84
msgid "Set config value"
msgstr ""
#: config.go:85
msgid "<key> <value>"
msgstr ""
#: config.go:118 config.go:126
msgid "invalid boolean value for %s: %s"
msgstr ""
#: config.go:141
msgid "use 'repo add/remove' commands to manage repositories"
msgstr ""
#: config.go:143 config.go:221
msgid "unknown config key: %s"
msgstr ""
#: config.go:147
msgid "failed to save config"
msgstr ""
#: config.go:150
msgid "Successfully set %s = %s"
msgstr ""
#: config.go:159
msgid "Get config value"
msgstr ""
#: config.go:160
msgid "<key>"
msgstr ""
#: fix.go:39
msgid "Attempt to fix problems with ALR"
msgstr ""
@ -138,11 +182,11 @@ msgstr ""
msgid "Can't detect system language"
msgstr ""
#: info.go:135
#: info.go:134
msgid "Error resolving overrides"
msgstr ""
#: info.go:144 info.go:149
#: info.go:143
msgid "Error encoding script variables"
msgstr ""
@ -174,19 +218,23 @@ msgstr ""
msgid "Error removing packages"
msgstr ""
#: internal/build/build.go:376
#: internal/build/build.go:351
msgid "Building package"
msgstr ""
#: internal/build/build.go:405
#: internal/build/build.go:380
msgid "The checksums array must be the same length as sources"
msgstr ""
#: internal/build/build.go:447
#: internal/build/build.go:422
msgid "Downloading sources"
msgstr ""
#: internal/build/build.go:539
#: internal/build/build.go:468
msgid "Would you like to remove the build dependencies?"
msgstr ""
#: internal/build/build.go:546
msgid "Installing dependencies"
msgstr ""
@ -220,7 +268,7 @@ msgstr ""
msgid "AutoReq is not implemented for this package format, so it's skipped"
msgstr ""
#: internal/build/firejail.go:59
#: internal/build/firejail.go:144
msgid "Applying FireJail integration"
msgstr ""
@ -355,47 +403,31 @@ msgid ""
"Database version does not exist. Run alr fix if something isn't working."
msgstr ""
#: internal/dl/dl.go:170
msgid "Source can be updated, updating if required"
msgstr ""
#: internal/dl/dl.go:201
msgid "Source found in cache and linked to destination"
msgstr ""
#: internal/dl/dl.go:208
msgid "Source updated and linked to destination"
msgstr ""
#: internal/dl/dl.go:222
msgid "Downloading source"
msgstr ""
#: internal/dl/progress_tui.go:100
msgid "%s: done!\n"
msgstr ""
#: internal/dl/progress_tui.go:104
msgid "%s %s downloading at %s/s\n"
msgstr ""
#: internal/logger/log.go:41
msgid "ERROR"
msgstr ""
#: internal/repos/pull.go:77
#: internal/repos/pull.go:97
msgid "Trying mirror"
msgstr ""
#: internal/repos/pull.go:103
msgid "Failed to pull from URL"
msgstr ""
#: internal/repos/pull.go:167
msgid "Pulling repository"
msgstr ""
#: internal/repos/pull.go:113
#: internal/repos/pull.go:204
msgid "Repository up to date"
msgstr ""
#: internal/repos/pull.go:204
#: internal/repos/pull.go:239
msgid "Git repository does not appear to be a valid ALR repo"
msgstr ""
#: internal/repos/pull.go:220
#: internal/repos/pull.go:255
msgid ""
"ALR repo's minimum ALR version is greater than the current version. Try "
"updating ALR if something doesn't work."
@ -413,30 +445,34 @@ msgstr ""
msgid "You need to be root to perform this action"
msgstr ""
#: list.go:43
#: list.go:45
msgid "List ALR repo packages"
msgstr ""
#: list.go:57
#: list.go:59
msgid "Format output using a Go template"
msgstr ""
#: list.go:89
#: list.go:91
msgid "Error getting packages for upgrade"
msgstr ""
#: list.go:92
#: list.go:94
msgid "No packages for upgrade"
msgstr ""
#: list.go:102 list.go:184
#: list.go:104 list.go:201
msgid "Error parsing format template"
msgstr ""
#: list.go:108 list.go:188
#: list.go:110 list.go:205
msgid "Error executing template"
msgstr ""
#: list.go:164
msgid "Failed to parse release"
msgstr ""
#: main.go:45
msgid "Print the current ALR version and exit"
msgstr ""
@ -449,75 +485,140 @@ msgstr ""
msgid "Enable interactive questions and prompts"
msgstr ""
#: main.go:146
#: main.go:148
msgid "Show help"
msgstr ""
#: main.go:150
#: main.go:152
msgid "Error while running app"
msgstr ""
#: pkg/dl/dl.go:170
msgid "Source can be updated, updating if required"
msgstr ""
#: pkg/dl/dl.go:196
msgid "Source found in cache and linked to destination"
msgstr ""
#: pkg/dl/dl.go:203
msgid "Source updated and linked to destination"
msgstr ""
#: pkg/dl/dl.go:217
msgid "Downloading source"
msgstr ""
#: pkg/dl/progress_tui.go:100
msgid "%s: done!\n"
msgstr ""
#: pkg/dl/progress_tui.go:104
msgid "%s %s downloading at %s/s\n"
msgstr ""
#: refresh.go:30
msgid "Pull all repositories that have changed"
msgstr ""
#: repo.go:39
#: repo.go:42
msgid "Manage repos"
msgstr ""
#: repo.go:51 repo.go:269
#: repo.go:56 repo.go:625
msgid "Remove an existing repository"
msgstr ""
#: repo.go:53
#: repo.go:58 repo.go:521
msgid "<name>"
msgstr ""
#: repo.go:83
#: repo.go:103 repo.go:465 repo.go:568
msgid "Repo \"%s\" does not exist"
msgstr ""
#: repo.go:90
#: repo.go:110
msgid "Error removing repo directory"
msgstr ""
#: repo.go:94 repo.go:161 repo.go:219
#: repo.go:114 repo.go:195 repo.go:253 repo.go:316 repo.go:389 repo.go:504
#: repo.go:576
msgid "Error saving config"
msgstr ""
#: repo.go:113
#: repo.go:133
msgid "Error removing packages from database"
msgstr ""
#: repo.go:124 repo.go:239
#: repo.go:144 repo.go:595
msgid "Add a new repository"
msgstr ""
#: repo.go:125
#: repo.go:145 repo.go:270 repo.go:345 repo.go:402
msgid "<name> <url>"
msgstr ""
#: repo.go:150
#: repo.go:170
msgid "Repo \"%s\" already exists"
msgstr ""
#: repo.go:187
#: repo.go:206
msgid "Set the reference of the repository"
msgstr ""
#: repo.go:188
#: repo.go:207
msgid "<name> <ref>"
msgstr ""
#: repo.go:246
#: repo.go:269
msgid "Set the main url of the repository"
msgstr ""
#: repo.go:332
msgid "Manage mirrors of repos"
msgstr ""
#: repo.go:344
msgid "Add a mirror URL to repository"
msgstr ""
#: repo.go:401
msgid "Remove mirror from the repository"
msgstr ""
#: repo.go:420
msgid "Ignore if mirror does not exist"
msgstr ""
#: repo.go:425
msgid "Match partial URL (e.g., github.com instead of full URL)"
msgstr ""
#: repo.go:490
msgid "No mirrors containing \"%s\" found in repo \"%s\""
msgstr ""
#: repo.go:492
msgid "URL \"%s\" does not exist in repo \"%s\""
msgstr ""
#: repo.go:508 repo.go:580
msgid "Removed %d mirrors from repo \"%s\"\n"
msgstr ""
#: repo.go:520
msgid "Remove all mirrors from the repository"
msgstr ""
#: repo.go:602
msgid "Name of the new repo"
msgstr ""
#: repo.go:252
#: repo.go:608
msgid "URL of the new repo"
msgstr ""
#: repo.go:276
#: repo.go:632
msgid "Name of the repo to be deleted"
msgstr ""

View File

@ -5,7 +5,7 @@
msgid ""
msgstr ""
"Project-Id-Version: unnamed project\n"
"PO-Revision-Date: 2025-06-15 16:05+0300\n"
"PO-Revision-Date: 2025-07-09 20:38+0300\n"
"Last-Translator: Maxim Slipenko <maks1ms@alt-gnome.ru>\n"
"Language-Team: Russian\n"
"Language: ru\n"
@ -16,55 +16,99 @@ msgstr ""
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
"X-Generator: Gtranslator 48.0\n"
#: build.go:42
#: build.go:41
msgid "Build a local package"
msgstr "Сборка локального пакета"
#: build.go:48
#: build.go:47
msgid "Path to the build script"
msgstr "Путь к скрипту сборки"
#: build.go:53
#: build.go:52
msgid "Specify subpackage in script (for multi package script only)"
msgstr "Укажите подпакет в скрипте (только для многопакетного скрипта)"
#: build.go:58
#: build.go:57
msgid "Name of the package to build and its repo (example: default/go-bin)"
msgstr "Имя пакета для сборки и его репозиторий (пример: default/go-bin)"
#: build.go:63
#: build.go:62
msgid ""
"Build package from scratch even if there's an already built package available"
msgstr "Создайте пакет с нуля, даже если уже имеется готовый пакет"
#: build.go:73
#: build.go:72
msgid "Error getting working directory"
msgstr "Ошибка при получении рабочего каталога"
#: build.go:118
#: build.go:117
msgid "Cannot get absolute script path"
msgstr "Невозможно получить абсолютный путь к скрипту"
#: build.go:152
#: build.go:143
msgid "Package not found"
msgstr "Пакет не найден"
#: build.go:165
#: build.go:156
msgid "Nothing to build"
msgstr "Нечего собирать"
#: build.go:222
#: build.go:213
msgid "Error building package"
msgstr "Ошибка при сборке пакета"
#: build.go:229
#: build.go:220
msgid "Error moving the package"
msgstr "Ошибка при перемещении пакета"
#: build.go:233
#: build.go:224
msgid "Done"
msgstr "Сделано"
#: config.go:36
msgid "Manage config"
msgstr "Управление конфигурацией"
#: config.go:48
msgid "Show config"
msgstr "Показать конфигурацию"
#: config.go:84
msgid "Set config value"
msgstr "Установить значение в конфигурации"
#: config.go:85
msgid "<key> <value>"
msgstr "<ключ> <значение>"
#: config.go:118 config.go:126
msgid "invalid boolean value for %s: %s"
msgstr "неверное булево значение для %s: %s"
#: config.go:141
msgid "use 'repo add/remove' commands to manage repositories"
msgstr "используйте команды 'repo add/remove' для управления репозиториями"
#: config.go:143 config.go:221
msgid "unknown config key: %s"
msgstr "неизвестный ключ конфигурации: %s"
#: config.go:147
msgid "failed to save config"
msgstr "не удалось сохранить конфигурацию"
#: config.go:150
msgid "Successfully set %s = %s"
msgstr "Успешно установлено %s = %s"
#: config.go:159
msgid "Get config value"
msgstr "Получить значение из конфигурации"
#: config.go:160
msgid "<key>"
msgstr "<ключ>"
#: fix.go:39
msgid "Attempt to fix problems with ALR"
msgstr "Попытка устранить проблемы с ALR"
@ -145,11 +189,11 @@ msgstr "Ошибка при поиске пакетов"
msgid "Can't detect system language"
msgstr "Ошибка при определении языка системы"
#: info.go:135
#: info.go:134
msgid "Error resolving overrides"
msgstr "Ошибка устранения переорпеделений"
#: info.go:144 info.go:149
#: info.go:143
msgid "Error encoding script variables"
msgstr "Ошибка кодирования переменных скрита"
@ -181,19 +225,23 @@ msgstr "Для команды remove ожидался хотя бы 1 аргум
msgid "Error removing packages"
msgstr "Ошибка при удалении пакетов"
#: internal/build/build.go:376
#: internal/build/build.go:351
msgid "Building package"
msgstr "Сборка пакета"
#: internal/build/build.go:405
#: internal/build/build.go:380
msgid "The checksums array must be the same length as sources"
msgstr "Массив контрольных сумм должен быть той же длины, что и источники"
#: internal/build/build.go:447
#: internal/build/build.go:422
msgid "Downloading sources"
msgstr "Скачивание источников"
#: internal/build/build.go:539
#: internal/build/build.go:468
msgid "Would you like to remove the build dependencies?"
msgstr "Хотели бы вы удалить зависимости сборки?"
#: internal/build/build.go:546
msgid "Installing dependencies"
msgstr "Установка зависимостей"
@ -231,7 +279,7 @@ msgid "AutoReq is not implemented for this package format, so it's skipped"
msgstr ""
"AutoReq не реализовано для этого формата пакета, поэтому будет пропущено"
#: internal/build/firejail.go:59
#: internal/build/firejail.go:144
msgid "Applying FireJail integration"
msgstr "Применение интеграции FireJail"
@ -369,47 +417,31 @@ msgid ""
msgstr ""
"Версия базы данных не существует. Запустите alr fix, если что-то не работает."
#: internal/dl/dl.go:170
msgid "Source can be updated, updating if required"
msgstr "Исходный код можно обновлять, обновляя при необходимости"
#: internal/dl/dl.go:201
msgid "Source found in cache and linked to destination"
msgstr "Источник найден в кэше и связан с пунктом назначения"
#: internal/dl/dl.go:208
msgid "Source updated and linked to destination"
msgstr "Источник обновлён и связан с пунктом назначения"
#: internal/dl/dl.go:222
msgid "Downloading source"
msgstr "Скачивание источника"
#: internal/dl/progress_tui.go:100
msgid "%s: done!\n"
msgstr "%s: выполнено!\n"
#: internal/dl/progress_tui.go:104
msgid "%s %s downloading at %s/s\n"
msgstr "%s %s загружается — %s/с\n"
#: internal/logger/log.go:41
msgid "ERROR"
msgstr "ОШИБКА"
#: internal/repos/pull.go:77
#: internal/repos/pull.go:97
msgid "Trying mirror"
msgstr "Пробую зеркало"
#: internal/repos/pull.go:103
msgid "Failed to pull from URL"
msgstr "Не удалось извлечь из URL"
#: internal/repos/pull.go:167
msgid "Pulling repository"
msgstr "Скачивание репозитория"
#: internal/repos/pull.go:113
#: internal/repos/pull.go:204
msgid "Repository up to date"
msgstr "Репозиторий уже обновлён"
#: internal/repos/pull.go:204
#: internal/repos/pull.go:239
msgid "Git repository does not appear to be a valid ALR repo"
msgstr "Репозиторий Git не поддерживается репозиторием ALR"
#: internal/repos/pull.go:220
#: internal/repos/pull.go:255
msgid ""
"ALR repo's minimum ALR version is greater than the current version. Try "
"updating ALR if something doesn't work."
@ -429,30 +461,34 @@ msgstr "Вы должны быть членом %s чтобы выполнить
msgid "You need to be root to perform this action"
msgstr "Вы должны быть root чтобы выполнить это"
#: list.go:43
#: list.go:45
msgid "List ALR repo packages"
msgstr "Список пакетов репозитория ALR"
#: list.go:57
#: list.go:59
msgid "Format output using a Go template"
msgstr "Формат выходных данных с использованием шаблона Go"
#: list.go:89
#: list.go:91
msgid "Error getting packages for upgrade"
msgstr "Ошибка при получении пакетов для обновления"
#: list.go:92
#: list.go:94
msgid "No packages for upgrade"
msgstr "Нет пакетов к обновлению"
#: list.go:102 list.go:184
#: list.go:104 list.go:201
msgid "Error parsing format template"
msgstr "Ошибка при разборе шаблона"
#: list.go:108 list.go:188
#: list.go:110 list.go:205
msgid "Error executing template"
msgstr "Ошибка при выполнении шаблона"
#: list.go:164
msgid "Failed to parse release"
msgstr "Не удалось разобрать релиз"
#: main.go:45
msgid "Print the current ALR version and exit"
msgstr "Показать текущую версию ALR и выйти"
@ -465,75 +501,140 @@ msgstr "Аргументы, которые будут переданы мене
msgid "Enable interactive questions and prompts"
msgstr "Включение интерактивных вопросов и запросов"
#: main.go:146
#: main.go:148
msgid "Show help"
msgstr "Показать справку"
#: main.go:150
#: main.go:152
msgid "Error while running app"
msgstr "Ошибка при запуске приложения"
#: pkg/dl/dl.go:170
msgid "Source can be updated, updating if required"
msgstr "Исходный код можно обновлять, обновляя при необходимости"
#: pkg/dl/dl.go:196
msgid "Source found in cache and linked to destination"
msgstr "Источник найден в кэше и связан с пунктом назначения"
#: pkg/dl/dl.go:203
msgid "Source updated and linked to destination"
msgstr "Источник обновлён и связан с пунктом назначения"
#: pkg/dl/dl.go:217
msgid "Downloading source"
msgstr "Скачивание источника"
#: pkg/dl/progress_tui.go:100
msgid "%s: done!\n"
msgstr "%s: выполнено!\n"
#: pkg/dl/progress_tui.go:104
msgid "%s %s downloading at %s/s\n"
msgstr "%s %s загружается — %s/с\n"
#: refresh.go:30
msgid "Pull all repositories that have changed"
msgstr "Скачать все изменённые репозитории"
#: repo.go:39
#: repo.go:42
msgid "Manage repos"
msgstr "Управление репозиториями"
#: repo.go:51 repo.go:269
#: repo.go:56 repo.go:625
msgid "Remove an existing repository"
msgstr "Удалить существующий репозиторий"
#: repo.go:53
#: repo.go:58 repo.go:521
msgid "<name>"
msgstr "<имя>"
#: repo.go:83
#: repo.go:103 repo.go:465 repo.go:568
msgid "Repo \"%s\" does not exist"
msgstr "Репозитория \"%s\" не существует"
#: repo.go:90
#: repo.go:110
msgid "Error removing repo directory"
msgstr "Ошибка при удалении каталога репозитория"
#: repo.go:94 repo.go:161 repo.go:219
#: repo.go:114 repo.go:195 repo.go:253 repo.go:316 repo.go:389 repo.go:504
#: repo.go:576
msgid "Error saving config"
msgstr "Ошибка при сохранении конфигурации"
#: repo.go:113
#: repo.go:133
msgid "Error removing packages from database"
msgstr "Ошибка при удалении пакетов из базы данных"
#: repo.go:124 repo.go:239
#: repo.go:144 repo.go:595
msgid "Add a new repository"
msgstr "Добавить новый репозиторий"
#: repo.go:125
#: repo.go:145 repo.go:270 repo.go:345 repo.go:402
msgid "<name> <url>"
msgstr "<имя> <url>"
#: repo.go:150
#: repo.go:170
msgid "Repo \"%s\" already exists"
msgstr "Репозиторий \"%s\" уже существует"
#: repo.go:187
#: repo.go:206
msgid "Set the reference of the repository"
msgstr "Установить ссылку на версию репозитория"
#: repo.go:188
#: repo.go:207
msgid "<name> <ref>"
msgstr "<имя> <ссылкааерсию>"
#: repo.go:246
#: repo.go:269
msgid "Set the main url of the repository"
msgstr "Установить главный URL репозитория"
#: repo.go:332
msgid "Manage mirrors of repos"
msgstr "Управление зеркалами репозитория"
#: repo.go:344
msgid "Add a mirror URL to repository"
msgstr "Добавить зеркало репозитория"
#: repo.go:401
msgid "Remove mirror from the repository"
msgstr "Удалить зеркало из репозитория"
#: repo.go:420
msgid "Ignore if mirror does not exist"
msgstr "Игнорировать, если зеркала не существует"
#: repo.go:425
msgid "Match partial URL (e.g., github.com instead of full URL)"
msgstr "Соответствует частичному URL (например, github.com вместо полного URL)"
#: repo.go:490
msgid "No mirrors containing \"%s\" found in repo \"%s\""
msgstr "В репозитории \"%s\" не найдено зеркал, содержащих \"%s\""
#: repo.go:492
msgid "URL \"%s\" does not exist in repo \"%s\""
msgstr "URL \"%s\" не существует в репозитории \"%s\""
#: repo.go:508 repo.go:580
msgid "Removed %d mirrors from repo \"%s\"\n"
msgstr "Удалены зеркала %d из репозитория \"%s\"\n"
#: repo.go:520
msgid "Remove all mirrors from the repository"
msgstr "Удалить все зеркала из репозитория"
#: repo.go:602
msgid "Name of the new repo"
msgstr "Название нового репозитория"
#: repo.go:252
#: repo.go:608
msgid "URL of the new repo"
msgstr "URL-адрес нового репозитория"
#: repo.go:276
#: repo.go:632
msgid "Name of the repo to be deleted"
msgstr "Название репозитория удалён"
@ -614,9 +715,6 @@ msgstr "Здесь нечего делать."
#~ msgid "Installing build dependencies"
#~ msgstr "Установка зависимостей сборки"
#~ msgid "Would you like to remove the build dependencies?"
#~ msgstr "Хотели бы вы удалить зависимости сборки?"
#~ msgid "Error installing native packages"
#~ msgstr "Ошибка при установке нативных пакетов"
@ -633,9 +731,6 @@ msgstr "Здесь нечего делать."
#~ msgid "Unable to detect user config directory"
#~ msgstr "Не удалось обнаружить каталог конфигурации пользователя"
#~ msgid "Unable to create ALR config file"
#~ msgstr "Не удалось создать конфигурационный файл ALR"
#~ msgid "Error encoding default configuration"
#~ msgstr "Ошибка кодирования конфигурации по умолчанию"

View File

@ -131,11 +131,11 @@ func EnsureIsAlrUser() error {
}
newUid := syscall.Getuid()
if newUid != uid {
return errors.New("new uid don't matches requested")
return errors.New("uid don't matches requested")
}
newGid := syscall.Getgid()
if newGid != gid {
return errors.New("new gid don't matches requested")
return errors.New("gid don't matches requested")
}
return nil
}

37
list.go
View File

@ -24,6 +24,7 @@ import (
"log/slog"
"os"
"slices"
"strings"
"text/template"
"github.com/leonelquinteros/gotext"
@ -33,6 +34,7 @@ import (
"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/manager"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/utils"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
)
@ -126,7 +128,12 @@ func ListCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Error getting packages"), err)
}
installedAlrPackages := map[string]string{}
type verInfo struct {
Version string
Release int
}
installedAlrPackages := map[string]verInfo{}
if c.Bool("installed") {
mgr := manager.Detect()
if mgr == nil {
@ -144,40 +151,50 @@ func ListCmd() *cli.Command {
if matches != nil {
packageName := matches[build.RegexpALRPackageName.SubexpIndex("package")]
repoName := matches[build.RegexpALRPackageName.SubexpIndex("repo")]
installedAlrPackages[fmt.Sprintf("%s/%s", repoName, packageName)] = version
verInfo := verInfo{
Version: version,
Release: 0,
}
if i := strings.LastIndex(version, "-"); i != -1 {
verInfo.Version = version[:i]
verInfo.Release, err = overrides.ParseReleasePlatformSpecific(version[i+1:], info)
if err != nil {
slog.Error(gotext.Get("Failed to parse release"), "err", err)
return cli.Exit(err, 1)
}
}
installedAlrPackages[fmt.Sprintf("%s/%s", repoName, packageName)] = verInfo
}
}
}
for _, pkg := range result {
if err != nil {
return cli.Exit(err, 1)
}
if slices.Contains(cfg.IgnorePkgUpdates(), pkg.Name) {
continue
}
type packageInfo struct {
Package *alrsh.Package
Version string
}
pkgInfo := &packageInfo{}
pkgInfo.Package = &pkg
pkgInfo.Version = pkg.Version
if c.Bool("installed") {
instVersion, ok := installedAlrPackages[fmt.Sprintf("%s/%s", pkg.Repository, pkg.Name)]
if !ok {
continue
} else {
pkgInfo.Version = instVersion
pkg.Version = instVersion.Version
pkg.Release = instVersion.Release
}
}
format := c.String("format")
if format == "" {
format = "{{.Package.Repository}}/{{.Package.Name}} {{.Version}}\n"
format = "{{.Package.Repository}}/{{.Package.Name}} {{.Package.Version}}-{{.Package.Release}}\n"
}
tmpl, err := template.New("format").Parse(format)
if err != nil {

View File

@ -83,10 +83,12 @@ func GetApp() *cli.App {
VersionCmd(),
SearchCmd(),
RepoCmd(),
ConfigCmd(),
// Internal commands
InternalBuildCmd(),
InternalInstallCmd(),
InternalMountCmd(),
InternalReposCmd(),
},
Before: func(c *cli.Context) error {
if trimmed := strings.TrimSpace(c.String("pm-args")); trimmed != "" {

View File

@ -68,10 +68,12 @@ func (o *OverridableField[T]) Resolve(overrides []string) {
for _, override := range overrides {
if v, ok := o.Has(override); ok {
o.SetResolved(v)
return
}
}
}
// Database serialization (JSON)
func (f *OverridableField[T]) ToDB() ([]byte, error) {
var data map[string]T
@ -103,6 +105,7 @@ func (f *OverridableField[T]) FromDB(data []byte) error {
return nil
}
// Gob serialization
type overridableFieldGobPayload[T any] struct {
Data map[string]T
Resolved T
@ -136,6 +139,48 @@ func (f *OverridableField[T]) GobDecode(data []byte) error {
return nil
}
type overridableFieldJSONPayload[T any] struct {
Resolved *T `json:"resolved,omitempty,omitzero"`
Data map[string]T `json:"overrides,omitempty,omitzero"`
}
func (f OverridableField[T]) MarshalJSON() ([]byte, error) {
data := make(map[string]T)
for k, v := range f.data {
if k == "" {
data["default"] = v
} else {
data[k] = v
}
}
payload := overridableFieldJSONPayload[T]{
Data: data,
Resolved: &f.resolved,
}
return json.Marshal(payload)
}
func (f *OverridableField[T]) UnmarshalJSON(data []byte) error {
var payload overridableFieldJSONPayload[T]
if err := json.Unmarshal(data, &payload); err != nil {
return err
}
if payload.Data == nil {
payload.Data = make(map[string]T)
}
f.data = payload.Data
if payload.Resolved != nil {
f.resolved = *payload.Resolved
}
return nil
}
func OverridableFromMap[T any](data map[string]T) OverridableField[T] {
if data == nil {
data = make(map[string]T)

View File

@ -14,9 +14,12 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//go:generate bash -c "go run ../../generators/alrsh-package && cd ../.. && make update-license"
package alrsh
import (
"encoding/json"
"fmt"
"reflect"
"strings"
@ -39,38 +42,38 @@ func ParseNames(dec *decoder.Decoder) (*PackageNames, error) {
}
type Package struct {
Repository string `xorm:"pk 'repository'"`
Name string `xorm:"pk 'name'"`
BasePkgName string `xorm:"notnull 'basepkg_name'"`
Repository string `xorm:"pk 'repository'" json:"repository"`
Name string `xorm:"pk 'name'" json:"name"`
BasePkgName string `xorm:"notnull 'basepkg_name'" json:"basepkg_name"`
Version string `sh:"version" xorm:"notnull 'version'"`
Release int `sh:"release" xorm:"notnull 'release'"`
Epoch uint `sh:"epoch" xorm:"'epoch'"`
Architectures []string `sh:"architectures" xorm:"json 'architectures'"`
Licenses []string `sh:"license" xorm:"json 'licenses'"`
Provides []string `sh:"provides" xorm:"json 'provides'"`
Conflicts []string `sh:"conflicts" xorm:"json 'conflicts'"`
Replaces []string `sh:"replaces" xorm:"json 'replaces'"`
Version string `sh:"version" xorm:"notnull 'version'" json:"version"`
Release int `sh:"release" xorm:"notnull 'release'" json:"release"`
Epoch uint `sh:"epoch" xorm:"'epoch'" json:"epoch"`
Architectures []string `sh:"architectures" xorm:"json 'architectures'" json:"architectures"`
Licenses []string `sh:"license" xorm:"json 'licenses'" json:"license"`
Provides []string `sh:"provides" xorm:"json 'provides'" json:"provides"`
Conflicts []string `sh:"conflicts" xorm:"json 'conflicts'" json:"conflicts"`
Replaces []string `sh:"replaces" xorm:"json 'replaces'" json:"replaces"`
Summary OverridableField[string] `sh:"summary" xorm:"'summary'"`
Description OverridableField[string] `sh:"desc" xorm:"'description'"`
Group OverridableField[string] `sh:"group" xorm:"'group_name'"`
Homepage OverridableField[string] `sh:"homepage" xorm:"'homepage'"`
Maintainer OverridableField[string] `sh:"maintainer" xorm:"'maintainer'"`
Depends OverridableField[[]string] `sh:"deps" xorm:"'depends'"`
BuildDepends OverridableField[[]string] `sh:"build_deps" xorm:"'builddepends'"`
OptDepends OverridableField[[]string] `sh:"opt_deps" xorm:"'optdepends'"`
Sources OverridableField[[]string] `sh:"sources" xorm:"-"`
Checksums OverridableField[[]string] `sh:"checksums" xorm:"-"`
Backup OverridableField[[]string] `sh:"backup" xorm:"-"`
Scripts OverridableField[Scripts] `sh:"scripts" xorm:"-"`
AutoReq OverridableField[[]string] `sh:"auto_req" xorm:"-"`
AutoProv OverridableField[[]string] `sh:"auto_prov" xorm:"-"`
AutoReqSkipList OverridableField[[]string] `sh:"auto_req_skiplist" xorm:"-"`
AutoProvSkipList OverridableField[[]string] `sh:"auto_prov_skiplist" xorm:"-"`
Summary OverridableField[string] `sh:"summary" xorm:"'summary'" json:"summary"`
Description OverridableField[string] `sh:"desc" xorm:"'description'" json:"description"`
Group OverridableField[string] `sh:"group" xorm:"'group_name'" json:"group"`
Homepage OverridableField[string] `sh:"homepage" xorm:"'homepage'" json:"homepage"`
Maintainer OverridableField[string] `sh:"maintainer" xorm:"'maintainer'" json:"maintainer"`
Depends OverridableField[[]string] `sh:"deps" xorm:"'depends'" json:"deps"`
BuildDepends OverridableField[[]string] `sh:"build_deps" xorm:"'builddepends'" json:"build_deps"`
OptDepends OverridableField[[]string] `sh:"opt_deps" xorm:"'optdepends'" json:"opt_deps,omitempty"`
Sources OverridableField[[]string] `sh:"sources" xorm:"-" json:"sources"`
Checksums OverridableField[[]string] `sh:"checksums" xorm:"-" json:"checksums,omitempty"`
Backup OverridableField[[]string] `sh:"backup" xorm:"-" json:"backup"`
Scripts OverridableField[Scripts] `sh:"scripts" xorm:"-" json:"scripts,omitempty"`
AutoReq OverridableField[[]string] `sh:"auto_req" xorm:"-" json:"auto_req"`
AutoProv OverridableField[[]string] `sh:"auto_prov" xorm:"-" json:"auto_prov"`
AutoReqSkipList OverridableField[[]string] `sh:"auto_req_skiplist" xorm:"-" json:"auto_req_skiplist,omitempty"`
AutoProvSkipList OverridableField[[]string] `sh:"auto_prov_skiplist" xorm:"-" json:"auto_prov_skiplist,omitempty"`
FireJailed OverridableField[bool] `sh:"firejailed" xorm:"-"`
FireJailProfiles OverridableField[map[string]string] `sh:"firejail_profiles" xorm:"-"`
FireJailed OverridableField[bool] `sh:"firejailed" xorm:"-" json:"firejailed"`
FireJailProfiles OverridableField[map[string]string] `sh:"firejail_profiles" xorm:"-" json:"firejail_profiles,omitempty"`
}
type Scripts struct {
@ -84,25 +87,70 @@ type Scripts struct {
PostTrans string `sh:"posttrans"`
}
func ResolvePackage(p *Package, overrides []string) {
val := reflect.ValueOf(p).Elem()
typ := val.Type()
func (p Package) MarshalJSONWithOptions(includeOverrides bool) ([]byte, error) {
// Сначала сериализуем обычным способом для получения базовой структуры
type PackageAlias Package
baseData, err := json.Marshal(PackageAlias(p))
if err != nil {
return nil, err
}
for i := range val.NumField() {
field := val.Field(i)
fieldType := typ.Field(i)
// Десериализуем в map для модификации
var result map[string]json.RawMessage
if err := json.Unmarshal(baseData, &result); err != nil {
return nil, err
}
if !field.CanInterface() {
// Теперь заменяем OverridableField поля
v := reflect.ValueOf(p)
t := reflect.TypeOf(p)
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := t.Field(i)
jsonTag := fieldType.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
if field.Kind() == reflect.Struct && strings.HasPrefix(fieldType.Type.String(), "alrsh.OverridableField") {
of := field.Addr().Interface()
if res, ok := of.(interface {
Resolve([]string)
}); ok {
res.Resolve(overrides)
fieldName := jsonTag
if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
fieldName = jsonTag[:commaIdx]
}
if field.Type().Name() == "OverridableField" ||
(field.Type().Kind() == reflect.Struct &&
strings.Contains(field.Type().String(), "OverridableField")) {
fieldPtr := field.Addr()
resolvedMethod := fieldPtr.MethodByName("Resolved")
if resolvedMethod.IsValid() {
resolved := resolvedMethod.Call(nil)[0]
fieldData := map[string]interface{}{
"resolved": resolved.Interface(),
}
if includeOverrides {
allMethod := field.MethodByName("All")
if allMethod.IsValid() {
overrides := allMethod.Call(nil)[0]
if !overrides.IsNil() && overrides.Len() > 0 {
fieldData["overrides"] = overrides.Interface()
}
}
}
fieldJSON, err := json.Marshal(fieldData)
if err != nil {
return nil, err
}
result[fieldName] = json.RawMessage(fieldJSON)
}
}
}
return json.Marshal(result)
}

105
pkg/alrsh/package_gen.go Normal file
View File

@ -0,0 +1,105 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
// DO NOT EDIT MANUALLY. This file is generated.
package alrsh
type packageResolved struct {
Repository string `json:"repository"`
Name string `json:"name"`
BasePkgName string `json:"basepkg_name"`
Version string `json:"version"`
Release int `json:"release"`
Epoch uint `json:"epoch"`
Architectures []string `json:"architectures"`
Licenses []string `json:"license"`
Provides []string `json:"provides"`
Conflicts []string `json:"conflicts"`
Replaces []string `json:"replaces"`
Summary string `json:"summary"`
Description string `json:"description"`
Group string `json:"group"`
Homepage string `json:"homepage"`
Maintainer string `json:"maintainer"`
Depends []string `json:"deps"`
BuildDepends []string `json:"build_deps"`
OptDepends []string `json:"opt_deps,omitempty"`
Sources []string `json:"sources"`
Checksums []string `json:"checksums,omitempty"`
Backup []string `json:"backup"`
Scripts Scripts `json:"scripts,omitempty"`
AutoReq []string `json:"auto_req"`
AutoProv []string `json:"auto_prov"`
AutoReqSkipList []string `json:"auto_req_skiplist,omitempty"`
AutoProvSkipList []string `json:"auto_prov_skiplist,omitempty"`
FireJailed bool `json:"firejailed"`
FireJailProfiles map[string]string `json:"firejail_profiles,omitempty"`
}
func PackageToResolved(src *Package) packageResolved {
return packageResolved{
Repository: src.Repository,
Name: src.Name,
BasePkgName: src.BasePkgName,
Version: src.Version,
Release: src.Release,
Epoch: src.Epoch,
Architectures: src.Architectures,
Licenses: src.Licenses,
Provides: src.Provides,
Conflicts: src.Conflicts,
Replaces: src.Replaces,
Summary: src.Summary.Resolved(),
Description: src.Description.Resolved(),
Group: src.Group.Resolved(),
Homepage: src.Homepage.Resolved(),
Maintainer: src.Maintainer.Resolved(),
Depends: src.Depends.Resolved(),
BuildDepends: src.BuildDepends.Resolved(),
OptDepends: src.OptDepends.Resolved(),
Sources: src.Sources.Resolved(),
Checksums: src.Checksums.Resolved(),
Backup: src.Backup.Resolved(),
Scripts: src.Scripts.Resolved(),
AutoReq: src.AutoReq.Resolved(),
AutoProv: src.AutoProv.Resolved(),
AutoReqSkipList: src.AutoReqSkipList.Resolved(),
AutoProvSkipList: src.AutoProvSkipList.Resolved(),
FireJailed: src.FireJailed.Resolved(),
FireJailProfiles: src.FireJailProfiles.Resolved(),
}
}
func ResolvePackage(pkg *Package, overrides []string) {
pkg.Summary.Resolve(overrides)
pkg.Description.Resolve(overrides)
pkg.Group.Resolve(overrides)
pkg.Homepage.Resolve(overrides)
pkg.Maintainer.Resolve(overrides)
pkg.Depends.Resolve(overrides)
pkg.BuildDepends.Resolve(overrides)
pkg.OptDepends.Resolve(overrides)
pkg.Sources.Resolve(overrides)
pkg.Checksums.Resolve(overrides)
pkg.Backup.Resolve(overrides)
pkg.Scripts.Resolve(overrides)
pkg.AutoReq.Resolve(overrides)
pkg.AutoProv.Resolve(overrides)
pkg.AutoReqSkipList.Resolve(overrides)
pkg.AutoProvSkipList.Resolve(overrides)
pkg.FireJailed.Resolve(overrides)
pkg.FireJailProfiles.Resolve(overrides)
}

37
pkg/alrsh/view.go Normal file
View File

@ -0,0 +1,37 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package alrsh
import "encoding/json"
type PackageView struct {
pkg Package
Resolved bool
}
func NewPackageView(v Package) PackageView {
return PackageView{pkg: v}
}
func (p PackageView) MarshalJSON() ([]byte, error) {
if p.Resolved {
return json.Marshal(PackageToResolved(&p.pkg))
} else {
return json.Marshal(p.pkg)
}
}

View File

@ -55,7 +55,7 @@ var (
// Массив доступных загрузчиков в порядке их проверки
var Downloaders = []Downloader{
GitDownloader{},
&GitDownloader{},
TorrentDownloader{},
FileDownloader{},
}
@ -172,15 +172,10 @@ func Download(ctx context.Context, opts Options) (err error) {
"downloader", d.Name(),
)
updated, err = d.Update(Options{
Hash: opts.Hash,
HashAlgorithm: opts.HashAlgorithm,
Name: opts.Name,
URL: opts.URL,
Destination: cacheDir,
Progress: opts.Progress,
LocalDir: opts.LocalDir,
})
newOpts := opts
newOpts.Destination = cacheDir
updated, err = d.Update(newOpts)
if err != nil {
return err
}
@ -226,15 +221,10 @@ func Download(ctx context.Context, opts Options) (err error) {
return err
}
t, name, err := d.Download(ctx, Options{
Hash: opts.Hash,
HashAlgorithm: opts.HashAlgorithm,
Name: opts.Name,
URL: opts.URL,
Destination: cacheDir,
Progress: opts.Progress,
LocalDir: opts.LocalDir,
})
newOpts := opts
newOpts.Destination = cacheDir
t, name, err := d.Download(ctx, newOpts)
if err != nil {
return err
}

View File

@ -32,8 +32,8 @@ import (
"github.com/stretchr/testify/assert"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"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/pkg/dl"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
)
type TestALRConfig struct{}
@ -155,7 +155,7 @@ func TestDownloadFileWithCache(t *testing.T) {
CacheDisabled: false,
URL: server.URL + "/file",
Destination: tmpdir,
DlCache: dlcache.New(cfg),
DlCache: dlcache.New(cfg.GetPaths().CacheDir),
}
outputFile := path.Join(tmpdir, "file")

View File

@ -108,7 +108,7 @@ func (FileDownloader) Download(ctx context.Context, opts Options) (Type, string,
}
defer r.Close()
opts.PostprocDisabled = archive == "false"
postprocDisabled := opts.PostprocDisabled || archive == "false"
path := filepath.Join(opts.Destination, name)
fl, err := os.Create(path)
@ -154,7 +154,7 @@ func (FileDownloader) Download(ctx context.Context, opts Options) (Type, string,
}
// Проверка необходимости постобработки
if opts.PostprocDisabled {
if postprocDisabled {
return TypeFile, name, nil
}

View File

@ -22,6 +22,7 @@ package dl
import (
"context"
"errors"
"fmt"
"net/url"
"path"
"strconv"
@ -48,7 +49,7 @@ func (GitDownloader) MatchURL(u string) bool {
// Download uses git to clone the repository from the specified URL.
// It allows specifying the revision, depth and recursion options
// via query string
func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string, error) {
func (d *GitDownloader) Download(ctx context.Context, opts Options) (Type, string, error) {
u, err := url.Parse(opts.URL)
if err != nil {
return 0, "", err
@ -60,6 +61,9 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
rev := query.Get("~rev")
query.Del("~rev")
// Right now, this only affects the return value of name,
// which will be used by dl_cache.
// It seems wrong, but for now it's better to leave it as it is.
name := query.Get("~name")
query.Del("~name")
@ -121,6 +125,11 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
}
}
err = VerifyHashFromLocal("", opts)
if err != nil {
return 0, "", err
}
if name == "" {
name = strings.TrimSuffix(path.Base(u.Path), ".git")
}
@ -133,7 +142,7 @@ func (GitDownloader) Download(ctx context.Context, opts Options) (Type, string,
// and recursion options via query string. It returns
// true if update was successful and false if the
// repository is already up-to-date
func (GitDownloader) Update(opts Options) (bool, error) {
func (d *GitDownloader) Update(opts Options) (bool, error) {
u, err := url.Parse(opts.URL)
if err != nil {
return false, err
@ -141,6 +150,7 @@ func (GitDownloader) Update(opts Options) (bool, error) {
u.Scheme = strings.TrimPrefix(u.Scheme, "git+")
query := u.Query()
rev := query.Get("~rev")
query.Del("~rev")
depthStr := query.Get("~depth")
@ -169,32 +179,75 @@ func (GitDownloader) Update(opts Options) (bool, error) {
}
}
po := &git.PullOptions{
Depth: depth,
Progress: opts.Progress,
RecurseSubmodules: git.NoRecurseSubmodules,
}
if recursive == "true" {
po.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth
// First, we do a fetch to get all the revisions.
fo := &git.FetchOptions{
Depth: depth,
Progress: opts.Progress,
}
m, err := getManifest(opts.Destination)
manifestOK := err == nil
err = w.Pull(po)
if errors.Is(err, git.NoErrAlreadyUpToDate) {
return false, nil
} else if err != nil {
err = r.Fetch(fo)
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return false, err
}
// If a revision is specified, switch to it.
if rev != "" {
// We are trying to find the revision as a hash of the commit
hash, err := r.ResolveRevision(plumbing.Revision(rev))
if err != nil {
return false, fmt.Errorf("failed to resolve revision %s: %w", rev, err)
}
err = w.Checkout(&git.CheckoutOptions{
Hash: *hash,
})
if err != nil {
return false, fmt.Errorf("failed to checkout revision %s: %w", rev, err)
}
if recursive == "true" {
submodules, err := w.Submodules()
if err == nil {
err = submodules.Update(&git.SubmoduleUpdateOptions{
Init: true,
})
if err != nil {
return false, fmt.Errorf("failed to update submodules %s: %w", rev, err)
}
}
}
} else {
// If the revision is not specified, we do a regular pull.
po := &git.PullOptions{
Depth: depth,
Progress: opts.Progress,
RecurseSubmodules: git.NoRecurseSubmodules,
}
if recursive == "true" {
po.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth
}
err = w.Pull(po)
if err != nil {
if errors.Is(err, git.NoErrAlreadyUpToDate) {
return false, nil
}
return false, err
}
}
err = VerifyHashFromLocal("", opts)
if err != nil {
return false, err
}
if manifestOK {
err = writeManifest(opts.Destination, m)
if err != nil {
return true, err
}
}
return true, nil
return true, err
}

183
pkg/dl/git_test.go Normal file
View File

@ -0,0 +1,183 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package dl_test
import (
"context"
"encoding/hex"
"os"
"os/exec"
"testing"
"github.com/stretchr/testify/assert"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dl"
)
func TestGitDownloaderMatchUrl(t *testing.T) {
d := dl.GitDownloader{}
assert.True(t, d.MatchURL("git+https://example.com/org/project.git"))
assert.False(t, d.MatchURL("https://example.com/org/project.git"))
}
func TestGitDownloaderDownload(t *testing.T) {
d := dl.GitDownloader{}
createTempDir := func(t *testing.T, name string) string {
t.Helper()
dir, err := os.MkdirTemp("", "test-"+name)
assert.NoError(t, err)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
return dir
}
t.Run("simple", func(t *testing.T) {
dest := createTempDir(t, "simple")
dlType, name, err := d.Download(context.Background(), dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git",
Destination: dest,
})
assert.NoError(t, err)
assert.Equal(t, dl.TypeDir, dlType)
assert.Equal(t, "repo-for-tests", name)
})
t.Run("with hash", func(t *testing.T) {
dest := createTempDir(t, "with-hash")
hsh, err := hex.DecodeString("33c912b855352663550003ca6b948ae3df1f38e2c036f5a85775df5967e143bf")
assert.NoError(t, err)
dlType, name, err := d.Download(context.Background(), dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git?~rev=init&~name=test",
Destination: dest,
Hash: hsh,
HashAlgorithm: "sha256",
})
assert.NoError(t, err)
assert.Equal(t, dl.TypeDir, dlType)
assert.Equal(t, "test", name)
})
t.Run("with hash (checksum mismatch)", func(t *testing.T) {
dest := createTempDir(t, "with-hash-checksum-mismatch")
hsh, err := hex.DecodeString("33c912b855352663550003ca6b948ae3df1f38e2c036f5a85775df5967e143bf")
assert.NoError(t, err)
_, _, err = d.Download(context.Background(), dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git",
Destination: dest,
Hash: hsh,
HashAlgorithm: "sha256",
})
assert.ErrorIs(t, err, dl.ErrChecksumMismatch)
})
}
func TestGitDownloaderUpdate(t *testing.T) {
d := dl.GitDownloader{}
createTempDir := func(t *testing.T, name string) string {
t.Helper()
dir, err := os.MkdirTemp("", "test-"+name)
assert.NoError(t, err)
t.Cleanup(func() {
_ = os.RemoveAll(dir)
})
return dir
}
setupOldRepo := func(t *testing.T, dest string) {
t.Helper()
cmd := exec.Command("git", "clone", "https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git", dest)
err := cmd.Run()
assert.NoError(t, err)
cmd = exec.Command("git", "-C", dest, "reset", "--hard", "init")
err = cmd.Run()
assert.NoError(t, err)
}
t.Run("simple", func(t *testing.T) {
dest := createTempDir(t, "update")
setupOldRepo(t, dest)
cmd := exec.Command("git", "-C", dest, "rev-parse", "HEAD")
oldHash, err := cmd.Output()
assert.NoError(t, err)
updated, err := d.Update(dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git",
Destination: dest,
})
assert.NoError(t, err)
assert.True(t, updated)
cmd = exec.Command("git", "-C", dest, "rev-parse", "HEAD")
newHash, err := cmd.Output()
assert.NoError(t, err)
assert.NotEqual(t, string(oldHash), string(newHash), "Repository should be updated")
})
t.Run("with hash", func(t *testing.T) {
dest := createTempDir(t, "update")
setupOldRepo(t, dest)
hsh, err := hex.DecodeString("0dc4f3c68c435d0cd7a5ee960f965815fa9c4ee0571839cdb8f9de56e06f91eb")
assert.NoError(t, err)
updated, err := d.Update(dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git?~rev=test-update-git-downloader",
Destination: dest,
Hash: hsh,
HashAlgorithm: "sha256",
})
assert.NoError(t, err)
assert.True(t, updated)
})
t.Run("with hash (checksum mismatch)", func(t *testing.T) {
dest := createTempDir(t, "update")
setupOldRepo(t, dest)
hsh, err := hex.DecodeString("33c912b855352663550003ca6b948ae3df1f38e2c036f5a85775df5967e143bf")
assert.NoError(t, err)
_, err = d.Update(dl.Options{
URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git?~rev=test-update-git-downloader",
Destination: dest,
Hash: hsh,
HashAlgorithm: "sha256",
})
assert.ErrorIs(t, err, dl.ErrChecksumMismatch)
})
}

View File

@ -71,7 +71,17 @@ func (TorrentDownloader) Download(ctx context.Context, opts Options) (Type, stri
return 0, "", err
}
return determineType(opts.Destination)
dlType, name, err := determineType(opts.Destination)
if err != nil {
return 0, "", err
}
err = VerifyHashFromLocal(name, opts)
if err != nil {
return 0, "", err
}
return dlType, name, nil
}
func removeTorrentFiles(path string) error {

95
pkg/dl/utils.go Normal file
View File

@ -0,0 +1,95 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package dl
import (
"bytes"
"encoding/hex"
"fmt"
"hash"
"io"
"log/slog"
"os"
"path/filepath"
)
// If the checksum does not match, returns ErrChecksumMismatch
func VerifyHashFromLocal(path string, opts Options) error {
if opts.Hash != nil {
h, err := opts.NewHash()
if err != nil {
return err
}
err = HashLocal(filepath.Join(opts.Destination, path), h)
if err != nil {
return err
}
sum := h.Sum(nil)
slog.Debug("validate checksum", "real", hex.EncodeToString(sum), "expected", hex.EncodeToString(opts.Hash))
if !bytes.Equal(sum, opts.Hash) {
return ErrChecksumMismatch
}
}
return nil
}
func HashLocal(path string, h hash.Hash) error {
info, err := os.Stat(path)
if err != nil {
return err
}
if info.Mode().IsRegular() {
// Single file
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(h, f)
return err
}
if info.IsDir() {
// Walk directory
return filepath.Walk(path, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() && info.Name() == ".git" {
return filepath.SkipDir
}
if !info.Mode().IsRegular() {
return nil
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(h, f)
return err
})
}
return fmt.Errorf("unsupported file type: %s", path)
}

View File

@ -32,19 +32,15 @@ type Config interface {
}
type DownloadCache struct {
cfg Config
cacheDir string
}
func New(cfg Config) *DownloadCache {
return &DownloadCache{
cfg,
}
func New(cacheDir string) *DownloadCache {
return &DownloadCache{cacheDir}
}
func (dc *DownloadCache) BasePath(ctx context.Context) string {
return filepath.Join(
dc.cfg.GetPaths().CacheDir, "dl",
)
return filepath.Join(dc.cacheDir, "dl")
}
// New creates a new directory with the given ID in the cache.

View File

@ -29,7 +29,7 @@ import (
"testing"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/dlcache"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/dlcache"
)
type TestALRConfig struct {
@ -64,7 +64,7 @@ func TestNew(t *testing.T) {
cfg := prepare(t)
defer cleanup(t, cfg)
dc := dlcache.New(cfg)
dc := dlcache.New(cfg.GetPaths().CacheDir)
ctx := context.Background()

View File

@ -21,18 +21,19 @@ package types
// Config represents the ALR configuration file
type Config struct {
RootCmd string `toml:"rootCmd" env:"ALR_ROOT_CMD"`
UseRootCmd bool `toml:"useRootCmd"`
PagerStyle string `toml:"pagerStyle" env:"ALR_PAGER_STYLE"`
IgnorePkgUpdates []string `toml:"ignorePkgUpdates"`
Repos []Repo `toml:"repo"`
AutoPull bool `toml:"autoPull" env:"ALR_AUTOPULL"`
LogLevel string `toml:"logLevel" env:"ALR_LOG_LEVEL"`
RootCmd string `json:"rootCmd" koanf:"rootCmd"`
UseRootCmd bool `json:"useRootCmd" koanf:"useRootCmd"`
PagerStyle string `json:"pagerStyle" koanf:"pagerStyle"`
IgnorePkgUpdates []string `json:"ignorePkgUpdates" koanf:"ignorePkgUpdates"`
Repos []Repo `json:"repo" koanf:"repo"`
AutoPull bool `json:"autoPull" koanf:"autoPull"`
LogLevel string `json:"logLevel" koanf:"logLevel"`
}
// Repo represents a ALR repo within a configuration file
type Repo struct {
Name string `toml:"name"`
URL string `toml:"url"`
Ref string `toml:"ref"`
Name string `json:"name" koanf:"name"`
URL string `json:"url" koanf:"url"`
Ref string `json:"ref" koanf:"ref"`
Mirrors []string `json:"mirrors" koanf:"mirrors"`
}

View File

@ -22,6 +22,9 @@ package types
// RepoConfig represents a ALR repo's alr-repo.toml file.
type RepoConfig struct {
Repo struct {
MinVersion string `toml:"minVersion"`
MinVersion string `toml:"minVersion"`
URL string `toml:"url"`
Ref string `toml:"ref"`
Mirrors []string `toml:"mirrors"`
}
}

396
repo.go
View File

@ -20,13 +20,16 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/leonelquinteros/gotext"
"github.com/urfave/cli/v2"
"golang.org/x/exp/slices"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/build"
"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"
@ -41,6 +44,8 @@ func RepoCmd() *cli.Command {
RemoveRepoCmd(),
AddRepoCmd(),
SetRepoRefCmd(),
RepoMirrorCmd(),
SetUrlCmd(),
},
}
}
@ -51,6 +56,21 @@ func RemoveRepoCmd() *cli.Command {
Usage: gotext.Get("Remove an existing repository"),
Aliases: []string{"rm"},
ArgsUsage: gotext.Get("<name>"),
BashComplete: func(c *cli.Context) {
if c.NArg() == 0 {
// Get repo names from config
ctx := c.Context
deps, err := appbuilder.New(ctx).WithConfig().Build()
if err != nil {
return
}
defer deps.Defer()
for _, repo := range deps.Cfg.Repos() {
fmt.Println(repo.Name)
}
}
},
Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 1 {
return cliutils.FormatCliExit("missing args", nil)
@ -89,7 +109,7 @@ func RemoveRepoCmd() *cli.Command {
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error removing repo directory"), err)
}
err = cfg.SaveUserConfig()
err = cfg.System.Save()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
@ -150,32 +170,31 @@ func AddRepoCmd() *cli.Command {
return cliutils.FormatCliExit(gotext.Get("Repo \"%s\" already exists", repo.Name), nil)
}
}
reposSlice = append(reposSlice, types.Repo{
newRepo := types.Repo{
Name: name,
URL: repoURL,
})
}
r, close, err := build.GetSafeReposExecutor()
if err != nil {
return err
}
defer close()
newRepo, err = r.PullOneAndUpdateFromConfig(c.Context, &newRepo)
if err != nil {
return err
}
reposSlice = append(reposSlice, newRepo)
cfg.SetRepos(reposSlice)
err = cfg.SaveUserConfig()
err = cfg.System.Save()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil {
return err
}
deps, err = appbuilder.
New(ctx).
UseConfig(cfg).
WithDB().
WithReposForcePull().
Build()
if err != nil {
return err
}
defer deps.Defer()
return nil
}),
}
@ -186,6 +205,21 @@ func SetRepoRefCmd() *cli.Command {
Name: "set-ref",
Usage: gotext.Get("Set the reference of the repository"),
ArgsUsage: gotext.Get("<name> <ref>"),
BashComplete: func(c *cli.Context) {
if c.NArg() == 0 {
// Get repo names from config
ctx := c.Context
deps, err := appbuilder.New(ctx).WithConfig().Build()
if err != nil {
return
}
defer deps.Defer()
for _, repo := range deps.Cfg.Repos() {
fmt.Println(repo.Name)
}
}
},
Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 2 {
return cliutils.FormatCliExit("missing args", nil)
@ -214,7 +248,7 @@ func SetRepoRefCmd() *cli.Command {
newRepos = append(newRepos, repo)
}
deps.Cfg.SetRepos(newRepos)
err = deps.Cfg.SaveUserConfig()
err = deps.Cfg.System.Save()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
@ -229,6 +263,328 @@ func SetRepoRefCmd() *cli.Command {
}
}
func SetUrlCmd() *cli.Command {
return &cli.Command{
Name: "set-url",
Usage: gotext.Get("Set the main url of the repository"),
ArgsUsage: gotext.Get("<name> <url>"),
BashComplete: func(c *cli.Context) {
if c.NArg() == 0 {
// Get repo names from config
ctx := c.Context
deps, err := appbuilder.New(ctx).WithConfig().Build()
if err != nil {
return
}
defer deps.Defer()
for _, repo := range deps.Cfg.Repos() {
fmt.Println(repo.Name)
}
}
},
Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 2 {
return cliutils.FormatCliExit("missing args", nil)
}
name := c.Args().Get(0)
repoUrl := c.Args().Get(1)
deps, err := appbuilder.
New(c.Context).
WithConfig().
WithDB().
WithReposNoPull().
Build()
if err != nil {
return err
}
defer deps.Defer()
repos := deps.Cfg.Repos()
newRepos := []types.Repo{}
for _, repo := range repos {
if repo.Name == name {
repo.URL = repoUrl
}
newRepos = append(newRepos, repo)
}
deps.Cfg.SetRepos(newRepos)
err = deps.Cfg.System.Save()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
err = deps.Repos.Pull(c.Context, newRepos)
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error pulling repositories"), err)
}
return nil
}),
}
}
func RepoMirrorCmd() *cli.Command {
return &cli.Command{
Name: "mirror",
Usage: gotext.Get("Manage mirrors of repos"),
Subcommands: []*cli.Command{
AddMirror(),
RemoveMirror(),
ClearMirrors(),
},
}
}
func AddMirror() *cli.Command {
return &cli.Command{
Name: "add",
Usage: gotext.Get("Add a mirror URL to repository"),
ArgsUsage: gotext.Get("<name> <url>"),
BashComplete: func(c *cli.Context) {
if c.NArg() == 0 {
ctx := c.Context
deps, err := appbuilder.New(ctx).WithConfig().Build()
if err != nil {
return
}
defer deps.Defer()
for _, repo := range deps.Cfg.Repos() {
fmt.Println(repo.Name)
}
}
},
Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 2 {
return cliutils.FormatCliExit("missing args", nil)
}
name := c.Args().Get(0)
url := c.Args().Get(1)
deps, err := appbuilder.
New(c.Context).
WithConfig().
WithDB().
WithReposNoPull().
Build()
if err != nil {
return err
}
defer deps.Defer()
repos := deps.Cfg.Repos()
for i, repo := range repos {
if repo.Name == name {
repos[i].Mirrors = append(repos[i].Mirrors, url)
break
}
}
deps.Cfg.SetRepos(repos)
err = deps.Cfg.System.Save()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
return nil
}),
}
}
func RemoveMirror() *cli.Command {
return &cli.Command{
Name: "remove",
Aliases: []string{"rm"},
Usage: gotext.Get("Remove mirror from the repository"),
ArgsUsage: gotext.Get("<name> <url>"),
BashComplete: func(c *cli.Context) {
ctx := c.Context
deps, err := appbuilder.New(ctx).WithConfig().Build()
if err != nil {
return
}
defer deps.Defer()
if c.NArg() == 0 {
for _, repo := range deps.Cfg.Repos() {
fmt.Println(repo.Name)
}
}
},
Flags: []cli.Flag{
&cli.BoolFlag{
Name: "ignore-missing",
Usage: gotext.Get("Ignore if mirror does not exist"),
},
&cli.BoolFlag{
Name: "partial",
Aliases: []string{"p"},
Usage: gotext.Get("Match partial URL (e.g., github.com instead of full URL)"),
},
},
Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 2 {
return cliutils.FormatCliExit("missing args", nil)
}
name := c.Args().Get(0)
urlToRemove := c.Args().Get(1)
ignoreMissing := c.Bool("ignore-missing")
partialMatch := c.Bool("partial")
deps, err := appbuilder.
New(c.Context).
WithConfig().
WithDB().
WithReposNoPull().
Build()
if err != nil {
return err
}
defer deps.Defer()
reposSlice := deps.Cfg.Repos()
repoIndex := -1
urlIndicesToRemove := []int{}
// Находим репозиторий
for i, repo := range reposSlice {
if repo.Name == name {
repoIndex = i
break
}
}
if repoIndex == -1 {
if ignoreMissing {
return nil // Тихо завершаем, если репозиторий не найден
}
return cliutils.FormatCliExit(gotext.Get("Repo \"%s\" does not exist", name), nil)
}
// Ищем зеркала для удаления
repo := reposSlice[repoIndex]
for j, mirror := range repo.Mirrors {
var match bool
if partialMatch {
// Частичное совпадение - проверяем, содержит ли зеркало указанную строку
match = strings.Contains(mirror, urlToRemove)
} else {
// Точное совпадение
match = mirror == urlToRemove
}
if match {
urlIndicesToRemove = append(urlIndicesToRemove, j)
}
}
if len(urlIndicesToRemove) == 0 {
if ignoreMissing {
return nil
}
if partialMatch {
return cliutils.FormatCliExit(gotext.Get("No mirrors containing \"%s\" found in repo \"%s\"", urlToRemove, name), nil)
} else {
return cliutils.FormatCliExit(gotext.Get("URL \"%s\" does not exist in repo \"%s\"", urlToRemove, name), nil)
}
}
for i := len(urlIndicesToRemove) - 1; i >= 0; i-- {
urlIndex := urlIndicesToRemove[i]
reposSlice[repoIndex].Mirrors = slices.Delete(reposSlice[repoIndex].Mirrors, urlIndex, urlIndex+1)
}
deps.Cfg.SetRepos(reposSlice)
err = deps.Cfg.System.Save()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
if len(urlIndicesToRemove) > 1 {
fmt.Println(gotext.Get("Removed %d mirrors from repo \"%s\"\n", len(urlIndicesToRemove), name))
}
return nil
}),
}
}
func ClearMirrors() *cli.Command {
return &cli.Command{
Name: "clear",
Aliases: []string{"rm-all"},
Usage: gotext.Get("Remove all mirrors from the repository"),
ArgsUsage: gotext.Get("<name>"),
BashComplete: func(c *cli.Context) {
if c.NArg() == 0 {
// Get repo names from config
ctx := c.Context
deps, err := appbuilder.New(ctx).WithConfig().Build()
if err != nil {
return
}
defer deps.Defer()
for _, repo := range deps.Cfg.Repos() {
fmt.Println(repo.Name)
}
}
},
Action: utils.RootNeededAction(func(c *cli.Context) error {
if c.Args().Len() < 1 {
return cliutils.FormatCliExit("missing args", nil)
}
name := c.Args().Get(0)
deps, err := appbuilder.
New(c.Context).
WithConfig().
WithDB().
WithReposNoPull().
Build()
if err != nil {
return err
}
defer deps.Defer()
reposSlice := deps.Cfg.Repos()
repoIndex := -1
urlIndicesToRemove := []int{}
// Находим репозиторий
for i, repo := range reposSlice {
if repo.Name == name {
repoIndex = i
break
}
}
if repoIndex == -1 {
return cliutils.FormatCliExit(gotext.Get("Repo \"%s\" does not exist", name), nil)
}
reposSlice[repoIndex].Mirrors = []string{}
deps.Cfg.SetRepos(reposSlice)
err = deps.Cfg.System.Save()
if err != nil {
return cliutils.FormatCliExit(gotext.Get("Error saving config"), err)
}
if len(urlIndicesToRemove) > 1 {
fmt.Println(gotext.Get("Removed %d mirrors from repo \"%s\"\n", len(urlIndicesToRemove), name))
}
return nil
}),
}
}
// TODO: remove
//
// Deprecated: use "alr repo add"