refactor: move pkg/ to internal/ and update imports

Restructure project by relocating package contents from pkg/ to internal/ to better reflect internal-only usage. This commit is initial step to prepare project for public api
This commit is contained in:
2025-06-09 10:15:47 +03:00
parent 06fcab4ce7
commit 703ab8e8c4
63 changed files with 209 additions and 209 deletions

View File

@ -1,738 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 (
"bytes"
"context"
"encoding/gob"
"errors"
"log/slog"
"github.com/leonelquinteros/gotext"
"mvdan.cc/sh/v3/syntax"
"mvdan.cc/sh/v3/syntax/typedjson"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
)
type BuildInput struct {
opts *types.BuildOpts
info *distro.OSRelease
pkgFormat string
script string
repository string
packages []string
}
func (bi *BuildInput) GobEncode() ([]byte, error) {
w := new(bytes.Buffer)
encoder := gob.NewEncoder(w)
if err := encoder.Encode(bi.opts); err != nil {
return nil, err
}
if err := encoder.Encode(bi.info); err != nil {
return nil, err
}
if err := encoder.Encode(bi.pkgFormat); err != nil {
return nil, err
}
if err := encoder.Encode(bi.script); err != nil {
return nil, err
}
if err := encoder.Encode(bi.repository); err != nil {
return nil, err
}
if err := encoder.Encode(bi.packages); err != nil {
return nil, err
}
return w.Bytes(), nil
}
func (bi *BuildInput) GobDecode(data []byte) error {
r := bytes.NewBuffer(data)
decoder := gob.NewDecoder(r)
if err := decoder.Decode(&bi.opts); err != nil {
return err
}
if err := decoder.Decode(&bi.info); err != nil {
return err
}
if err := decoder.Decode(&bi.pkgFormat); err != nil {
return err
}
if err := decoder.Decode(&bi.script); err != nil {
return err
}
if err := decoder.Decode(&bi.repository); err != nil {
return err
}
if err := decoder.Decode(&bi.packages); err != nil {
return err
}
return nil
}
func (b *BuildInput) Repository() string {
return b.repository
}
func (b *BuildInput) BuildOpts() *types.BuildOpts {
return b.opts
}
func (b *BuildInput) OSRelease() *distro.OSRelease {
return b.info
}
func (b *BuildInput) PkgFormat() string {
return b.pkgFormat
}
type BuildOptsProvider interface {
BuildOpts() *types.BuildOpts
}
type OsInfoProvider interface {
OSRelease() *distro.OSRelease
}
type PkgFormatProvider interface {
PkgFormat() string
}
type RepositoryProvider interface {
Repository() string
}
// ================================================
type ScriptFile struct {
File *syntax.File
Path string
}
func (s *ScriptFile) GobEncode() ([]byte, error) {
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(s.Path); err != nil {
return nil, err
}
var fileBuf bytes.Buffer
if err := typedjson.Encode(&fileBuf, s.File); err != nil {
return nil, err
}
fileData := fileBuf.Bytes()
if err := enc.Encode(fileData); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func (s *ScriptFile) GobDecode(data []byte) error {
buf := bytes.NewBuffer(data)
dec := gob.NewDecoder(buf)
if err := dec.Decode(&s.Path); err != nil {
return err
}
var fileData []byte
if err := dec.Decode(&fileData); err != nil {
return err
}
fileReader := bytes.NewReader(fileData)
file, err := typedjson.Decode(fileReader)
if err != nil {
return err
}
s.File = file.(*syntax.File)
return nil
}
type BuiltDep struct {
Name string
Path string
}
func Map[T, R any](items []T, f func(T) R) []R {
res := make([]R, len(items))
for i, item := range items {
res[i] = f(item)
}
return res
}
func GetBuiltPaths(deps []*BuiltDep) []string {
return Map(deps, func(dep *BuiltDep) string {
return dep.Path
})
}
func GetBuiltName(deps []*BuiltDep) []string {
return Map(deps, func(dep *BuiltDep) string {
return dep.Name
})
}
type PackageFinder interface {
FindPkgs(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error)
}
type Config interface {
GetPaths() *config.Paths
PagerStyle() string
}
type FunctionsOutput struct {
Contents *[]string
}
// EXECUTORS
type ScriptResolverExecutor interface {
ResolveScript(ctx context.Context, pkg *db.Package) *ScriptInfo
}
type ScriptExecutor interface {
ReadScript(ctx context.Context, scriptPath string) (*ScriptFile, error)
ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *ScriptFile) (string, []*types.BuildVars, error)
PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error
ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *ScriptFile,
varsOfPackages []*types.BuildVars,
repoDeps []string,
builtDeps []*BuiltDep,
basePkg string,
) ([]*BuiltDep, error)
}
type CacheExecutor interface {
CheckForBuiltPackage(ctx context.Context, input *BuildInput, vars *types.BuildVars) (string, bool, error)
}
type ScriptViewerExecutor interface {
ViewScript(ctx context.Context, input *BuildInput, sf *ScriptFile, basePkg string) error
}
type CheckerExecutor interface {
PerformChecks(
ctx context.Context,
input *BuildInput,
vars *types.BuildVars,
) (bool, error)
}
type InstallerExecutor interface {
InstallLocal(paths []string, opts *manager.Opts) error
Install(pkgs []string, opts *manager.Opts) error
RemoveAlreadyInstalled(pkgs []string) ([]string, error)
}
type SourcesInput struct {
Sources []string
Checksums []string
}
type SourceDownloaderExecutor interface {
DownloadSources(
ctx context.Context,
input *BuildInput,
basePkg string,
si SourcesInput,
) error
}
//
func NewBuilder(
scriptResolver ScriptResolverExecutor,
scriptExecutor ScriptExecutor,
cacheExecutor CacheExecutor,
scriptViewerExecutor ScriptViewerExecutor,
checkerExecutor CheckerExecutor,
installerExecutor InstallerExecutor,
sourceExecutor SourceDownloaderExecutor,
) *Builder {
return &Builder{
scriptResolver: scriptResolver,
scriptExecutor: scriptExecutor,
cacheExecutor: cacheExecutor,
scriptViewerExecutor: scriptViewerExecutor,
checkerExecutor: checkerExecutor,
installerExecutor: installerExecutor,
sourceExecutor: sourceExecutor,
}
}
type Builder struct {
scriptResolver ScriptResolverExecutor
scriptExecutor ScriptExecutor
cacheExecutor CacheExecutor
scriptViewerExecutor ScriptViewerExecutor
checkerExecutor CheckerExecutor
installerExecutor InstallerExecutor
sourceExecutor SourceDownloaderExecutor
repos PackageFinder
// mgr manager.Manager
}
type BuildArgs struct {
Opts *types.BuildOpts
Info *distro.OSRelease
PkgFormat_ string
}
func (b *BuildArgs) BuildOpts() *types.BuildOpts {
return b.Opts
}
func (b *BuildArgs) OSRelease() *distro.OSRelease {
return b.Info
}
func (b *BuildArgs) PkgFormat() string {
return b.PkgFormat_
}
type BuildPackageFromDbArgs struct {
BuildArgs
Package *db.Package
Packages []string
}
type BuildPackageFromScriptArgs struct {
BuildArgs
Script string
Packages []string
}
func (b *Builder) BuildPackageFromDb(
ctx context.Context,
args *BuildPackageFromDbArgs,
) ([]*BuiltDep, error) {
scriptInfo := b.scriptResolver.ResolveScript(ctx, args.Package)
return b.BuildPackage(ctx, &BuildInput{
script: scriptInfo.Script,
repository: scriptInfo.Repository,
packages: args.Packages,
pkgFormat: args.PkgFormat(),
opts: args.Opts,
info: args.Info,
})
}
func (b *Builder) BuildPackageFromScript(
ctx context.Context,
args *BuildPackageFromScriptArgs,
) ([]*BuiltDep, error) {
return b.BuildPackage(ctx, &BuildInput{
script: args.Script,
repository: "default",
packages: args.Packages,
pkgFormat: args.PkgFormat(),
opts: args.Opts,
info: args.Info,
})
}
func (b *Builder) BuildPackage(
ctx context.Context,
input *BuildInput,
) ([]*BuiltDep, error) {
scriptPath := input.script
slog.Debug("ReadScript")
sf, err := b.scriptExecutor.ReadScript(ctx, scriptPath)
if err != nil {
return nil, err
}
slog.Debug("ExecuteFirstPass")
basePkg, varsOfPackages, err := b.scriptExecutor.ExecuteFirstPass(ctx, input, sf)
if err != nil {
return nil, err
}
var builtDeps []*BuiltDep
if !input.opts.Clean {
var remainingVars []*types.BuildVars
for _, vars := range varsOfPackages {
builtPkgPath, ok, err := b.cacheExecutor.CheckForBuiltPackage(ctx, input, vars)
if err != nil {
return nil, err
}
if ok {
builtDeps = append(builtDeps, &BuiltDep{
Path: builtPkgPath,
})
} else {
remainingVars = append(remainingVars, vars)
}
}
if len(remainingVars) == 0 {
return builtDeps, nil
}
}
slog.Debug("ViewScript")
err = b.scriptViewerExecutor.ViewScript(ctx, input, sf, basePkg)
if err != nil {
return nil, err
}
slog.Info(gotext.Get("Building package"), "name", basePkg)
for _, vars := range varsOfPackages {
cont, err := b.checkerExecutor.PerformChecks(ctx, input, vars)
if err != nil {
return nil, err
}
if !cont {
return nil, errors.New("exit...")
}
}
buildDepends := []string{}
optDepends := []string{}
depends := []string{}
sources := []string{}
checksums := []string{}
for _, vars := range varsOfPackages {
buildDepends = append(buildDepends, vars.BuildDepends...)
optDepends = append(optDepends, vars.OptDepends...)
depends = append(depends, vars.Depends...)
sources = append(sources, vars.Sources...)
checksums = append(checksums, vars.Checksums...)
}
buildDepends = removeDuplicates(buildDepends)
optDepends = removeDuplicates(optDepends)
depends = removeDuplicates(depends)
if len(sources) != len(checksums) {
slog.Error(gotext.Get("The checksums array must be the same length as sources"))
return nil, errors.New("exit...")
}
sources, checksums = removeDuplicatesSources(sources, checksums)
slog.Debug("installBuildDeps")
alrBuildDeps, err := b.installBuildDeps(ctx, input, buildDepends)
if err != nil {
return nil, err
}
slog.Debug("installOptDeps")
_, err = b.installOptDeps(ctx, input, optDepends)
if err != nil {
return nil, err
}
depNames := make(map[string]struct{})
for _, dep := range alrBuildDeps {
depNames[dep.Name] = struct{}{}
}
// We filter so as not to re-build what has already been built at the `installBuildDeps` stage.
var filteredDepends []string
for _, d := range depends {
if _, found := depNames[d]; !found {
filteredDepends = append(filteredDepends, d)
}
}
slog.Debug("BuildALRDeps")
newBuiltDeps, repoDeps, err := b.BuildALRDeps(ctx, input, filteredDepends)
if err != nil {
return nil, err
}
slog.Debug("PrepareDirs")
err = b.scriptExecutor.PrepareDirs(ctx, input, basePkg)
if err != nil {
return nil, err
}
slog.Info(gotext.Get("Downloading sources"))
slog.Debug("DownloadSources")
err = b.sourceExecutor.DownloadSources(
ctx,
input,
basePkg,
SourcesInput{
Sources: sources,
Checksums: checksums,
},
)
if err != nil {
return nil, err
}
builtDeps = removeDuplicates(append(builtDeps, newBuiltDeps...))
slog.Debug("ExecuteSecondPass")
res, err := b.scriptExecutor.ExecuteSecondPass(
ctx,
input,
sf,
varsOfPackages,
repoDeps,
builtDeps,
basePkg,
)
if err != nil {
return nil, err
}
builtDeps = removeDuplicates(append(builtDeps, res...))
return builtDeps, nil
}
type InstallPkgsArgs struct {
BuildArgs
AlrPkgs []db.Package
NativePkgs []string
}
func (b *Builder) InstallALRPackages(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
alrPkgs []db.Package,
) error {
for _, pkg := range alrPkgs {
res, err := b.BuildPackageFromDb(
ctx,
&BuildPackageFromDbArgs{
Package: &pkg,
Packages: []string{},
BuildArgs: BuildArgs{
Opts: input.BuildOpts(),
Info: input.OSRelease(),
PkgFormat_: input.PkgFormat(),
},
},
)
if err != nil {
return err
}
err = b.installerExecutor.InstallLocal(
GetBuiltPaths(res),
&manager.Opts{
NoConfirm: !input.BuildOpts().Interactive,
},
)
if err != nil {
return err
}
}
return nil
}
func (b *Builder) BuildALRDeps(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
depends []string,
) (buildDeps []*BuiltDep, repoDeps []string, err error) {
if len(depends) > 0 {
slog.Info(gotext.Get("Installing dependencies"))
found, notFound, err := b.repos.FindPkgs(ctx, depends) // Поиск зависимостей
if err != nil {
return nil, nil, err
}
repoDeps = notFound
// Если для некоторых пакетов есть несколько опций, упрощаем их все в один срез
pkgs := cliutils.FlattenPkgs(
ctx,
found,
"install",
input.BuildOpts().Interactive,
)
type item struct {
pkg *db.Package
packages []string
}
pkgsMap := make(map[string]*item)
for _, pkg := range pkgs {
name := pkg.BasePkgName
if name == "" {
name = pkg.Name
}
if pkgsMap[name] == nil {
pkgsMap[name] = &item{
pkg: &pkg,
}
}
pkgsMap[name].packages = append(
pkgsMap[name].packages,
pkg.Name,
)
}
for basePkgName := range pkgsMap {
pkg := pkgsMap[basePkgName].pkg
res, err := b.BuildPackageFromDb(
ctx,
&BuildPackageFromDbArgs{
Package: pkg,
Packages: pkgsMap[basePkgName].packages,
BuildArgs: BuildArgs{
Opts: input.BuildOpts(),
Info: input.OSRelease(),
PkgFormat_: input.PkgFormat(),
},
},
)
if err != nil {
return nil, nil, err
}
buildDeps = append(buildDeps, res...)
}
}
repoDeps = removeDuplicates(repoDeps)
buildDeps = removeDuplicates(buildDeps)
return buildDeps, repoDeps, nil
}
func (i *Builder) installBuildDeps(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
pkgs []string,
) ([]*BuiltDep, error) {
var builtDeps []*BuiltDep
if len(pkgs) > 0 {
deps, err := i.installerExecutor.RemoveAlreadyInstalled(pkgs)
if err != nil {
return nil, err
}
builtDeps, err = i.InstallPkgs(ctx, input, deps) // Устанавливаем выбранные пакеты
if err != nil {
return nil, err
}
}
return builtDeps, nil
}
func (i *Builder) installOptDeps(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
pkgs []string,
) ([]*BuiltDep, error) {
var builtDeps []*BuiltDep
optDeps, err := i.installerExecutor.RemoveAlreadyInstalled(pkgs)
if err != nil {
return nil, err
}
if len(optDeps) > 0 {
optDeps, err := cliutils.ChooseOptDepends(
ctx,
optDeps,
"install",
input.BuildOpts().Interactive,
) // Пользователя просят выбрать опциональные зависимости
if err != nil {
return nil, err
}
if len(optDeps) == 0 {
return builtDeps, nil
}
builtDeps, err = i.InstallPkgs(ctx, input, optDeps) // Устанавливаем выбранные пакеты
if err != nil {
return nil, err
}
}
return builtDeps, nil
}
func (i *Builder) InstallPkgs(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
},
pkgs []string,
) ([]*BuiltDep, error) {
builtDeps, repoDeps, err := i.BuildALRDeps(ctx, input, pkgs)
if err != nil {
return nil, err
}
if len(builtDeps) > 0 {
err = i.installerExecutor.InstallLocal(GetBuiltPaths(builtDeps), &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive,
})
if err != nil {
return nil, err
}
}
if len(repoDeps) > 0 {
err = i.installerExecutor.Install(repoDeps, &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive,
})
if err != nil {
return nil, err
}
}
return builtDeps, nil
}

View File

@ -1,286 +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"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"mvdan.cc/sh/v3/syntax"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
)
type TestPackageFinder struct {
FindPkgsFunc func(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error)
}
func (pf *TestPackageFinder) FindPkgs(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error) {
if pf.FindPkgsFunc != nil {
return pf.FindPkgsFunc(ctx, pkgs)
}
return map[string][]db.Package{}, []string{}, nil
}
type TestManager struct {
NameFunc func() string
FormatFunc func() string
ExistsFunc func() bool
SetRootCmdFunc func(cmd string)
SyncFunc func(opts *manager.Opts) error
InstallFunc func(opts *manager.Opts, pkgs ...string) error
RemoveFunc func(opts *manager.Opts, pkgs ...string) error
UpgradeFunc func(opts *manager.Opts, pkgs ...string) error
InstallLocalFunc func(opts *manager.Opts, files ...string) error
UpgradeAllFunc func(opts *manager.Opts) error
ListInstalledFunc func(opts *manager.Opts) (map[string]string, error)
IsInstalledFunc func(pkg string) (bool, error)
}
func (m *TestManager) Name() string {
if m.NameFunc != nil {
return m.NameFunc()
}
return "TestManager"
}
func (m *TestManager) Format() string {
if m.FormatFunc != nil {
return m.FormatFunc()
}
return "testpkg"
}
func (m *TestManager) Exists() bool {
if m.ExistsFunc != nil {
return m.ExistsFunc()
}
return true
}
func (m *TestManager) SetRootCmd(cmd string) {
if m.SetRootCmdFunc != nil {
m.SetRootCmdFunc(cmd)
}
}
func (m *TestManager) Sync(opts *manager.Opts) error {
if m.SyncFunc != nil {
return m.SyncFunc(opts)
}
return nil
}
func (m *TestManager) Install(opts *manager.Opts, pkgs ...string) error {
if m.InstallFunc != nil {
return m.InstallFunc(opts, pkgs...)
}
return nil
}
func (m *TestManager) Remove(opts *manager.Opts, pkgs ...string) error {
if m.RemoveFunc != nil {
return m.RemoveFunc(opts, pkgs...)
}
return nil
}
func (m *TestManager) Upgrade(opts *manager.Opts, pkgs ...string) error {
if m.UpgradeFunc != nil {
return m.UpgradeFunc(opts, pkgs...)
}
return nil
}
func (m *TestManager) InstallLocal(opts *manager.Opts, files ...string) error {
if m.InstallLocalFunc != nil {
return m.InstallLocalFunc(opts, files...)
}
return nil
}
func (m *TestManager) UpgradeAll(opts *manager.Opts) error {
if m.UpgradeAllFunc != nil {
return m.UpgradeAllFunc(opts)
}
return nil
}
func (m *TestManager) ListInstalled(opts *manager.Opts) (map[string]string, error) {
if m.ListInstalledFunc != nil {
return m.ListInstalledFunc(opts)
}
return map[string]string{}, nil
}
func (m *TestManager) IsInstalled(pkg string) (bool, error) {
if m.IsInstalledFunc != nil {
return m.IsInstalledFunc(pkg)
}
return true, nil
}
type TestConfig struct{}
func (c *TestConfig) PagerStyle() string {
return "native"
}
func (c *TestConfig) GetPaths() *config.Paths {
return &config.Paths{
CacheDir: "/tmp",
}
}
func TestExecuteFirstPassIsSecure(t *testing.T) {
cfg := &TestConfig{}
pf := &TestPackageFinder{}
info := &distro.OSRelease{}
m := &TestManager{}
opts := types.BuildOpts{
Manager: m,
Interactive: false,
}
ctx := context.Background()
b := NewBuilder(
ctx,
opts,
pf,
info,
cfg,
)
tmpFile, err := os.CreateTemp("", "testfile-")
assert.NoError(t, err)
tmpFilePath := tmpFile.Name()
defer os.Remove(tmpFilePath)
_, err = os.Stat(tmpFilePath)
assert.NoError(t, err)
testScript := fmt.Sprintf(`name='test'
version=1.0.0
release=1
rm -f %s`, tmpFilePath)
fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "alr.sh")
assert.NoError(t, err)
_, _, err = b.executeFirstPass(fl)
assert.NoError(t, err)
_, err = os.Stat(tmpFilePath)
assert.NoError(t, err)
}
func TestExecuteFirstPassIsCorrect(t *testing.T) {
type testCase struct {
Name string
Script string
Opts types.BuildOpts
Expected func(t *testing.T, vars []*types.BuildVars)
}
for _, tc := range []testCase{{
Name: "single package",
Script: `name='test'
version='1.0.0'
release=1
epoch=2
desc="Test package"
homepage='https://example.com'
maintainer='Ivan Ivanov'
`,
Opts: types.BuildOpts{
Manager: &TestManager{},
Interactive: false,
},
Expected: func(t *testing.T, vars []*types.BuildVars) {
assert.Equal(t, 1, len(vars))
assert.Equal(t, vars[0].Name, "test")
assert.Equal(t, vars[0].Version, "1.0.0")
assert.Equal(t, vars[0].Release, int(1))
assert.Equal(t, vars[0].Epoch, uint(2))
assert.Equal(t, vars[0].Description, "Test package")
},
}, {
Name: "multiple packages",
Script: `name=(
foo
bar
)
version='0.0.1'
release=1
epoch=2
desc="Test package"
meta_foo() {
desc="Foo package"
}
meta_bar() {
}
`,
Opts: types.BuildOpts{
Packages: []string{"foo"},
Manager: &TestManager{},
Interactive: false,
},
Expected: func(t *testing.T, vars []*types.BuildVars) {
assert.Equal(t, 1, len(vars))
assert.Equal(t, vars[0].Name, "foo")
assert.Equal(t, vars[0].Description, "Foo package")
},
}} {
t.Run(tc.Name, func(t *testing.T) {
cfg := &TestConfig{}
pf := &TestPackageFinder{}
info := &distro.OSRelease{}
ctx := context.Background()
b := NewBuilder(
ctx,
tc.Opts,
pf,
info,
cfg,
)
fl, err := syntax.NewParser().Parse(strings.NewReader(tc.Script), "alr.sh")
assert.NoError(t, err)
_, allVars, err := b.scriptExecutor.ExecuteSecondPass(fl)
assert.NoError(t, err)
tc.Expected(t, allVars)
})
}
}

View File

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

View File

@ -1,74 +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"
"log/slog"
"github.com/leonelquinteros/gotext"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
)
type Checker struct {
mgr manager.Manager
}
func (c *Checker) PerformChecks(
ctx context.Context,
input *BuildInput,
vars *types.BuildVars,
) (bool, error) {
if !cpu.IsCompatibleWith(cpu.Arch(), vars.Architectures) { // Проверяем совместимость архитектуры
cont, err := cliutils.YesNoPrompt(
ctx,
gotext.Get("Your system's CPU architecture doesn't match this package. Do you want to build anyway?"),
input.opts.Interactive,
true,
)
if err != nil {
return false, err
}
if !cont {
return false, nil
}
}
installed, err := c.mgr.ListInstalled(nil)
if err != nil {
return false, err
}
filename, err := pkgFileName(input, vars)
if err != nil {
return false, err
}
if instVer, ok := installed[filename]; ok { // Если пакет уже установлен, выводим предупреждение
slog.Warn(gotext.Get("This package is already installed"),
"name", vars.Name,
"version", instVer,
)
}
return true, nil
}

View File

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

View File

@ -1,96 +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 finddeps
import (
"bytes"
"context"
"log/slog"
"os/exec"
"path"
"strings"
"github.com/goreleaser/nfpm/v2"
"github.com/leonelquinteros/gotext"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
)
func rpmFindDependenciesALTLinux(ctx context.Context, pkgInfo *nfpm.Info, dirs types.Directories, command string, envs []string, updateFunc func(string)) error {
if _, err := exec.LookPath(command); err != nil {
slog.Info(gotext.Get("Command not found on the system"), "command", command)
return nil
}
var paths []string
for _, content := range pkgInfo.Contents {
if content.Type != "dir" {
paths = append(paths,
path.Join(dirs.PkgDir, content.Destination),
)
}
}
if len(paths) == 0 {
return nil
}
cmd := exec.CommandContext(ctx, command)
cmd.Stdin = bytes.NewBufferString(strings.Join(paths, "\n") + "\n")
cmd.Env = append(cmd.Env,
"RPM_BUILD_ROOT="+dirs.PkgDir,
"RPM_FINDPROV_METHOD=",
"RPM_FINDREQ_METHOD=",
"RPM_DATADIR=",
"RPM_SUBPACKAGE_NAME=",
)
cmd.Env = append(cmd.Env, envs...)
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
slog.Error(stderr.String())
return err
}
slog.Debug(stderr.String())
dependencies := strings.Split(strings.TrimSpace(out.String()), "\n")
for _, dep := range dependencies {
if dep != "" {
updateFunc(dep)
}
}
return nil
}
type ALTLinuxFindProvReq struct{}
func (o *ALTLinuxFindProvReq) FindProvides(ctx context.Context, pkgInfo *nfpm.Info, dirs types.Directories, skiplist []string) error {
return rpmFindDependenciesALTLinux(ctx, pkgInfo, dirs, "/usr/lib/rpm/find-provides", []string{"RPM_FINDPROV_SKIPLIST=" + strings.Join(skiplist, "\n")}, func(dep string) {
slog.Info(gotext.Get("Provided dependency found"), "dep", dep)
pkgInfo.Overridables.Provides = append(pkgInfo.Overridables.Provides, dep)
})
}
func (o *ALTLinuxFindProvReq) FindRequires(ctx context.Context, pkgInfo *nfpm.Info, dirs types.Directories, skiplist []string) error {
return rpmFindDependenciesALTLinux(ctx, pkgInfo, dirs, "/usr/lib/rpm/find-requires", []string{"RPM_FINDREQ_SKIPLIST=" + strings.Join(skiplist, "\n")}, func(dep string) {
slog.Info(gotext.Get("Required dependency found"), "dep", dep)
pkgInfo.Overridables.Depends = append(pkgInfo.Overridables.Depends, dep)
})
}

View File

@ -1,39 +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 finddeps
import (
"context"
"log/slog"
"github.com/goreleaser/nfpm/v2"
"github.com/leonelquinteros/gotext"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
)
type EmptyFindProvReq struct{}
func (o *EmptyFindProvReq) FindProvides(ctx context.Context, pkgInfo *nfpm.Info, dirs types.Directories, skiplist []string) error {
slog.Info(gotext.Get("AutoProv is not implemented for this package format, so it's skipped"))
return nil
}
func (o *EmptyFindProvReq) FindRequires(ctx context.Context, pkgInfo *nfpm.Info, dirs types.Directories, skiplist []string) error {
slog.Info(gotext.Get("AutoReq is not implemented for this package format, so it's skipped"))
return nil
}

View File

@ -1,118 +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 finddeps
import (
"bytes"
"context"
"fmt"
"log/slog"
"os/exec"
"path"
"strings"
"github.com/goreleaser/nfpm/v2"
"github.com/leonelquinteros/gotext"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
)
type FedoraFindProvReq struct{}
func rpmFindDependenciesFedora(ctx context.Context, pkgInfo *nfpm.Info, dirs types.Directories, command string, args []string, updateFunc func(string)) error {
if _, err := exec.LookPath(command); err != nil {
slog.Info(gotext.Get("Command not found on the system"), "command", command)
return nil
}
var paths []string
for _, content := range pkgInfo.Contents {
if content.Type != "dir" {
paths = append(paths,
path.Join(dirs.PkgDir, content.Destination),
)
}
}
if len(paths) == 0 {
return nil
}
cmd := exec.CommandContext(ctx, command, args...)
cmd.Stdin = bytes.NewBufferString(strings.Join(paths, "\n") + "\n")
cmd.Env = append(cmd.Env,
"RPM_BUILD_ROOT="+dirs.PkgDir,
)
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
slog.Error(stderr.String())
return err
}
slog.Debug(stderr.String())
dependencies := strings.Split(strings.TrimSpace(out.String()), "\n")
for _, dep := range dependencies {
if dep != "" {
updateFunc(dep)
}
}
return nil
}
func (o *FedoraFindProvReq) FindProvides(ctx context.Context, pkgInfo *nfpm.Info, dirs types.Directories, skiplist []string) error {
return rpmFindDependenciesFedora(
ctx,
pkgInfo,
dirs,
"/usr/lib/rpm/rpmdeps",
[]string{
"--define=_use_internal_dependency_generator 1",
"--provides",
fmt.Sprintf(
"--define=__provides_exclude_from %s\"",
strings.Join(skiplist, "|"),
),
},
func(dep string) {
slog.Info(gotext.Get("Provided dependency found"), "dep", dep)
pkgInfo.Overridables.Provides = append(pkgInfo.Overridables.Provides, dep)
})
}
func (o *FedoraFindProvReq) FindRequires(ctx context.Context, pkgInfo *nfpm.Info, dirs types.Directories, skiplist []string) error {
return rpmFindDependenciesFedora(
ctx,
pkgInfo,
dirs,
"/usr/lib/rpm/rpmdeps",
[]string{
"--define=_use_internal_dependency_generator 1",
"--requires",
fmt.Sprintf(
"--define=__requires_exclude_from %s",
strings.Join(skiplist, "|"),
),
},
func(dep string) {
slog.Info(gotext.Get("Required dependency found"), "dep", dep)
pkgInfo.Overridables.Depends = append(pkgInfo.Overridables.Depends, dep)
})
}

View File

@ -1,58 +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 finddeps
import (
"context"
"github.com/goreleaser/nfpm/v2"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
)
type ProvReqFinder interface {
FindProvides(ctx context.Context, pkgInfo *nfpm.Info, dirs types.Directories, skiplist []string) error
FindRequires(ctx context.Context, pkgInfo *nfpm.Info, dirs types.Directories, skiplist []string) error
}
type ProvReqService struct {
finder ProvReqFinder
}
func New(info *distro.OSRelease, pkgFormat string) *ProvReqService {
s := &ProvReqService{
finder: &EmptyFindProvReq{},
}
if pkgFormat == "rpm" {
switch info.ID {
case "altlinux":
s.finder = &ALTLinuxFindProvReq{}
case "fedora":
s.finder = &FedoraFindProvReq{}
}
}
return s
}
func (s *ProvReqService) FindProvides(ctx context.Context, pkgInfo *nfpm.Info, dirs types.Directories, skiplist []string) error {
return s.finder.FindProvides(ctx, pkgInfo, dirs, skiplist)
}
func (s *ProvReqService) FindRequires(ctx context.Context, pkgInfo *nfpm.Info, dirs types.Directories, skiplist []string) error {
return s.finder.FindRequires(ctx, pkgInfo, dirs, skiplist)
}

View File

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

View File

@ -1,52 +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 (
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
)
func NewMainBuilder(
cfg Config,
mgr manager.Manager,
repos PackageFinder,
scriptExecutor ScriptExecutor,
installerExecutor InstallerExecutor,
) (*Builder, error) {
builder := &Builder{
scriptExecutor: scriptExecutor,
cacheExecutor: &Cache{
cfg,
},
scriptResolver: &ScriptResolver{
cfg,
},
scriptViewerExecutor: &ScriptViewer{
config: cfg,
},
checkerExecutor: &Checker{
mgr,
},
installerExecutor: installerExecutor,
sourceExecutor: &SourceDownloader{
cfg,
},
repos: repos,
}
return builder, nil
}

View File

@ -1,40 +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 (
"os"
"os/exec"
"strings"
)
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)
}
}
}

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/pkg/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/internal/types"
)
var HandshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "ALR_PLUGIN",
MagicCookieValue: "-",
}
type ScriptExecutorPlugin struct {
Impl ScriptExecutor
}
type ScriptExecutorRPCServer struct {
Impl ScriptExecutor
}
// =============================
//
// ReadScript
//
func (s *ScriptExecutorRPC) ReadScript(ctx context.Context, scriptPath string) (*ScriptFile, error) {
var resp *ScriptFile
err := s.client.Call("Plugin.ReadScript", scriptPath, &resp)
return resp, err
}
func (s *ScriptExecutorRPCServer) ReadScript(scriptPath string, resp *ScriptFile) error {
file, err := s.Impl.ReadScript(context.Background(), scriptPath)
if err != nil {
return err
}
*resp = *file
return nil
}
// =============================
//
// ExecuteFirstPass
//
type ExecuteFirstPassArgs struct {
Input *BuildInput
Sf *ScriptFile
}
type ExecuteFirstPassResp struct {
BasePkg string
VarsOfPackages []*types.BuildVars
}
func (s *ScriptExecutorRPC) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *ScriptFile) (string, []*types.BuildVars, error) {
var resp *ExecuteFirstPassResp
err := s.client.Call("Plugin.ExecuteFirstPass", &ExecuteFirstPassArgs{
Input: input,
Sf: sf,
}, &resp)
if err != nil {
return "", nil, err
}
return resp.BasePkg, resp.VarsOfPackages, nil
}
func (s *ScriptExecutorRPCServer) ExecuteFirstPass(args *ExecuteFirstPassArgs, resp *ExecuteFirstPassResp) error {
basePkg, varsOfPackages, err := s.Impl.ExecuteFirstPass(context.Background(), args.Input, args.Sf)
if err != nil {
return err
}
*resp = ExecuteFirstPassResp{
BasePkg: basePkg,
VarsOfPackages: varsOfPackages,
}
return nil
}
// =============================
//
// PrepareDirs
//
type PrepareDirsArgs struct {
Input *BuildInput
BasePkg string
}
func (s *ScriptExecutorRPC) PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error {
err := s.client.Call("Plugin.PrepareDirs", &PrepareDirsArgs{
Input: input,
BasePkg: basePkg,
}, nil)
if err != nil {
return err
}
return err
}
func (s *ScriptExecutorRPCServer) PrepareDirs(args *PrepareDirsArgs, reply *struct{}) error {
err := s.Impl.PrepareDirs(
context.Background(),
args.Input,
args.BasePkg,
)
if err != nil {
return err
}
return err
}
// =============================
//
// ExecuteSecondPass
//
type ExecuteSecondPassArgs struct {
Input *BuildInput
Sf *ScriptFile
VarsOfPackages []*types.BuildVars
RepoDeps []string
BuiltDeps []*BuiltDep
BasePkg string
}
func (s *ScriptExecutorRPC) ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *ScriptFile,
varsOfPackages []*types.BuildVars,
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

@ -1,445 +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 (
"bytes"
"context"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"slices"
"strconv"
"strings"
"time"
"github.com/google/shlex"
"github.com/goreleaser/nfpm/v2"
"github.com/leonelquinteros/gotext"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/handlers"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/helpers"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
finddeps "gitea.plemya-x.ru/Plemya-x/ALR/pkg/build/find_deps"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
)
type LocalScriptExecutor struct {
cfg Config
}
func NewLocalScriptExecutor(cfg Config) *LocalScriptExecutor {
return &LocalScriptExecutor{
cfg,
}
}
func (e *LocalScriptExecutor) ReadScript(ctx context.Context, scriptPath string) (*ScriptFile, error) {
fl, err := readScript(scriptPath)
if err != nil {
return nil, err
}
return &ScriptFile{
Path: scriptPath,
File: fl,
}, nil
}
func (e *LocalScriptExecutor) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *ScriptFile) (string, []*types.BuildVars, error) {
varsOfPackages := []*types.BuildVars{}
scriptDir := filepath.Dir(sf.Path)
env := createBuildEnvVars(input.info, types.Directories{ScriptDir: scriptDir})
runner, err := interp.New(
interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение
interp.StdIO(os.Stdin, os.Stderr, os.Stderr), // Устанавливаем стандартный ввод-вывод
interp.ExecHandler(helpers.Restricted.ExecHandler(handlers.NopExec)), // Ограничиваем выполнение
interp.ReadDirHandler2(handlers.RestrictedReadDir(scriptDir)), // Ограничиваем чтение директорий
interp.StatHandler(handlers.RestrictedStat(scriptDir)), // Ограничиваем доступ к статистике файлов
interp.OpenHandler(handlers.RestrictedOpen(scriptDir)), // Ограничиваем открытие файлов
interp.Dir(scriptDir),
)
if err != nil {
return "", nil, err
}
err = runner.Run(ctx, sf.File) // Запускаем скрипт
if err != nil {
return "", nil, err
}
dec := decoder.New(input.info, runner) // Создаём новый декодер
type packages struct {
BasePkgName string `sh:"basepkg_name"`
Names []string `sh:"name"`
}
var pkgs packages
err = dec.DecodeVars(&pkgs)
if err != nil {
return "", nil, err
}
if len(pkgs.Names) == 0 {
return "", nil, errors.New("package name is missing")
}
var vars types.BuildVars
if len(pkgs.Names) == 1 {
err = dec.DecodeVars(&vars) // Декодируем переменные
if err != nil {
return "", nil, err
}
varsOfPackages = append(varsOfPackages, &vars)
return vars.Name, varsOfPackages, nil
}
var pkgNames []string
if len(input.packages) != 0 {
pkgNames = input.packages
} else {
pkgNames = pkgs.Names
}
for _, pkgName := range pkgNames {
var preVars types.BuildVarsPre
funcName := fmt.Sprintf("meta_%s", pkgName)
meta, ok := dec.GetFuncWithSubshell(funcName)
if !ok {
return "", nil, fmt.Errorf("func %s is missing", funcName)
}
r, err := meta(ctx)
if err != nil {
return "", nil, err
}
d := decoder.New(&distro.OSRelease{}, r)
err = d.DecodeVars(&preVars)
if err != nil {
return "", nil, err
}
vars := preVars.ToBuildVars()
vars.Name = pkgName
vars.Base = pkgs.BasePkgName
varsOfPackages = append(varsOfPackages, &vars)
}
return pkgs.BasePkgName, varsOfPackages, nil
}
func (e *LocalScriptExecutor) PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error {
dirs, err := getDirs(
e.cfg,
input.script,
basePkg,
)
if err != nil {
return err
}
err = prepareDirs(dirs)
if err != nil {
return err
}
return nil
}
func (e *LocalScriptExecutor) ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *ScriptFile,
varsOfPackages []*types.BuildVars,
repoDeps []string,
builtDeps []*BuiltDep,
basePkg string,
) ([]*BuiltDep, error) {
dirs, err := getDirs(e.cfg, sf.Path, basePkg)
if err != nil {
return nil, err
}
env := createBuildEnvVars(input.info, dirs)
fakeroot := handlers.FakerootExecHandler(2 * time.Second)
runner, err := interp.New(
interp.Env(expand.ListEnviron(env...)), // Устанавливаем окружение
interp.StdIO(os.Stdin, os.Stderr, os.Stderr), // Устанавливаем стандартный ввод-вывод
interp.ExecHandlers(func(next interp.ExecHandlerFunc) interp.ExecHandlerFunc {
return helpers.Helpers.ExecHandler(fakeroot)
}), // Обрабатываем выполнение через fakeroot
)
if err != nil {
return nil, err
}
err = runner.Run(ctx, sf.File)
if err != nil {
return nil, err
}
dec := decoder.New(input.info, runner)
// var builtPaths []string
err = e.ExecuteFunctions(ctx, dirs, dec)
if err != nil {
return nil, err
}
for _, vars := range varsOfPackages {
packageName := ""
if vars.Base != "" {
packageName = vars.Name
}
pkgFormat := input.pkgFormat
funcOut, err := e.ExecutePackageFunctions(
ctx,
dec,
dirs,
packageName,
)
if err != nil {
return nil, err
}
slog.Info(gotext.Get("Building package metadata"), "name", basePkg)
pkgInfo, err := buildPkgMetadata(
ctx,
input,
vars,
dirs,
append(
repoDeps,
GetBuiltName(builtDeps)...,
),
funcOut.Contents,
)
if err != nil {
return nil, err
}
packager, err := nfpm.Get(pkgFormat) // Получаем упаковщик для формата пакета
if err != nil {
return nil, err
}
pkgName := packager.ConventionalFileName(pkgInfo) // Получаем имя файла пакета
pkgPath := filepath.Join(dirs.BaseDir, pkgName) // Определяем путь к пакету
pkgFile, err := os.Create(pkgPath)
if err != nil {
return nil, err
}
err = packager.Package(pkgInfo, pkgFile)
if err != nil {
return nil, err
}
builtDeps = append(builtDeps, &BuiltDep{
Name: vars.Name,
Path: pkgPath,
})
}
return builtDeps, nil
}
func buildPkgMetadata(
ctx context.Context,
input interface {
OsInfoProvider
BuildOptsProvider
PkgFormatProvider
RepositoryProvider
},
vars *types.BuildVars,
dirs types.Directories,
deps []string,
preferedContents *[]string,
) (*nfpm.Info, error) {
pkgInfo := getBasePkgInfo(vars, input)
pkgInfo.Description = vars.Description
pkgInfo.Platform = "linux"
pkgInfo.Homepage = vars.Homepage
pkgInfo.License = strings.Join(vars.Licenses, ", ")
pkgInfo.Maintainer = vars.Maintainer
pkgInfo.Overridables = nfpm.Overridables{
Conflicts: append(vars.Conflicts, vars.Name),
Replaces: vars.Replaces,
Provides: append(vars.Provides, vars.Name),
Depends: deps,
}
pkgInfo.Section = vars.Group
pkgFormat := input.PkgFormat()
info := input.OSRelease()
if pkgFormat == "apk" {
// Alpine отказывается устанавливать пакеты, которые предоставляют сами себя, поэтому удаляем такие элементы
pkgInfo.Overridables.Provides = slices.DeleteFunc(pkgInfo.Overridables.Provides, func(s string) bool {
return s == pkgInfo.Name
})
}
if pkgFormat == "rpm" {
pkgInfo.RPM.Group = vars.Group
if vars.Summary != "" {
pkgInfo.RPM.Summary = vars.Summary
} else {
lines := strings.SplitN(vars.Description, "\n", 2)
pkgInfo.RPM.Summary = lines[0]
}
}
if vars.Epoch != 0 {
pkgInfo.Epoch = strconv.FormatUint(uint64(vars.Epoch), 10)
}
setScripts(vars, pkgInfo, dirs.ScriptDir)
if slices.Contains(vars.Architectures, "all") {
pkgInfo.Arch = "all"
}
contents, err := buildContents(vars, dirs, preferedContents)
if err != nil {
return nil, err
}
pkgInfo.Overridables.Contents = contents
if len(vars.AutoProv) == 1 && decoder.IsTruthy(vars.AutoProv[0]) {
f := finddeps.New(info, pkgFormat)
err = f.FindProvides(ctx, pkgInfo, dirs, vars.AutoProvSkipList)
if err != nil {
return nil, err
}
}
if len(vars.AutoReq) == 1 && decoder.IsTruthy(vars.AutoReq[0]) {
f := finddeps.New(info, pkgFormat)
err = f.FindRequires(ctx, pkgInfo, dirs, vars.AutoReqSkipList)
if err != nil {
return nil, err
}
}
return pkgInfo, nil
}
func (e *LocalScriptExecutor) ExecuteFunctions(ctx context.Context, dirs types.Directories, dec *decoder.Decoder) error {
prepare, ok := dec.GetFunc("prepare")
if ok {
slog.Info(gotext.Get("Executing prepare()"))
err := prepare(ctx, interp.Dir(dirs.SrcDir))
if err != nil {
return err
}
}
build, ok := dec.GetFunc("build")
if ok {
slog.Info(gotext.Get("Executing build()"))
err := build(ctx, interp.Dir(dirs.SrcDir))
if err != nil {
return err
}
}
return nil
}
func (e *LocalScriptExecutor) ExecutePackageFunctions(
ctx context.Context,
dec *decoder.Decoder,
dirs types.Directories,
packageName string,
) (*FunctionsOutput, error) {
output := &FunctionsOutput{}
var packageFuncName string
var filesFuncName string
if packageName == "" {
packageFuncName = "package"
filesFuncName = "files"
} else {
packageFuncName = fmt.Sprintf("package_%s", packageName)
filesFuncName = fmt.Sprintf("files_%s", packageName)
}
packageFn, ok := dec.GetFunc(packageFuncName)
if ok {
slog.Info(gotext.Get("Executing %s()", packageFuncName))
err := packageFn(ctx, interp.Dir(dirs.SrcDir))
if err != nil {
return nil, err
}
}
files, ok := dec.GetFuncP(filesFuncName, func(ctx context.Context, s *interp.Runner) error {
// It should be done via interp.RunnerOption,
// but due to the issues below, it cannot be done.
// - https://github.com/mvdan/sh/issues/962
// - https://github.com/mvdan/sh/issues/1125
script, err := syntax.NewParser().Parse(strings.NewReader("cd $pkgdir && shopt -s globstar"), "")
if err != nil {
return err
}
return s.Run(ctx, script)
})
if ok {
slog.Info(gotext.Get("Executing %s()", filesFuncName))
buf := &bytes.Buffer{}
err := files(
ctx,
interp.Dir(dirs.PkgDir),
interp.StdIO(os.Stdin, buf, os.Stderr),
)
if err != nil {
return nil, err
}
contents, err := shlex.Split(buf.String())
if err != nil {
return nil, err
}
output.Contents = &contents
}
return output, nil
}

View File

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

View File

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

View File

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

View File

@ -1,324 +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"
"io"
"os"
"path/filepath"
"regexp"
"runtime"
"slices"
"strconv"
"strings"
// Импортируем пакеты для поддержки различных форматов пакетов (APK, DEB, RPM и ARCH).
_ "github.com/goreleaser/nfpm/v2/apk"
_ "github.com/goreleaser/nfpm/v2/arch"
_ "github.com/goreleaser/nfpm/v2/deb"
_ "github.com/goreleaser/nfpm/v2/rpm"
"mvdan.cc/sh/v3/syntax"
"github.com/goreleaser/nfpm/v2"
"github.com/goreleaser/nfpm/v2/files"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager"
)
// Функция readScript анализирует скрипт сборки с использованием встроенной реализации bash
func readScript(script string) (*syntax.File, error) {
fl, err := os.Open(script) // Открываем файл скрипта
if err != nil {
return nil, err
}
defer fl.Close() // Закрываем файл после выполнения
file, err := syntax.NewParser().Parse(fl, "alr.sh") // Парсим скрипт с помощью синтаксического анализатора
if err != nil {
return nil, err
}
return file, nil // Возвращаем синтаксическое дерево
}
// Функция prepareDirs подготавливает директории для сборки.
func prepareDirs(dirs types.Directories) error {
err := os.RemoveAll(dirs.BaseDir) // Удаляем базовую директорию, если она существует
if err != nil {
return err
}
err = os.MkdirAll(dirs.SrcDir, 0o755) // Создаем директорию для источников
if err != nil {
return err
}
return os.MkdirAll(dirs.PkgDir, 0o755) // Создаем директорию для пакетов
}
// Функция buildContents создает секцию содержимого пакета, которая содержит файлы,
// которые будут включены в конечный пакет.
func buildContents(vars *types.BuildVars, dirs types.Directories, preferedContents *[]string) ([]*files.Content, error) {
contents := []*files.Content{}
processPath := func(path, trimmed string, prefered bool) error {
fi, err := os.Lstat(path)
if err != nil {
return err
}
if fi.IsDir() {
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
if !prefered {
_, err = f.Readdirnames(1)
if err != io.EOF {
return nil
}
}
contents = append(contents, &files.Content{
Source: path,
Destination: trimmed,
Type: "dir",
FileInfo: &files.ContentFileInfo{
MTime: fi.ModTime(),
},
})
return nil
}
if fi.Mode()&os.ModeSymlink != 0 {
link, err := os.Readlink(path)
if err != nil {
return err
}
link = strings.TrimPrefix(link, dirs.PkgDir)
contents = append(contents, &files.Content{
Source: link,
Destination: trimmed,
Type: "symlink",
FileInfo: &files.ContentFileInfo{
MTime: fi.ModTime(),
Mode: fi.Mode(),
},
})
return nil
}
fileContent := &files.Content{
Source: path,
Destination: trimmed,
FileInfo: &files.ContentFileInfo{
MTime: fi.ModTime(),
Mode: fi.Mode(),
Size: fi.Size(),
},
}
if slices.Contains(vars.Backup, trimmed) {
fileContent.Type = "config|noreplace"
}
contents = append(contents, fileContent)
return nil
}
if preferedContents != nil {
for _, trimmed := range *preferedContents {
path := filepath.Join(dirs.PkgDir, trimmed)
if err := processPath(path, trimmed, true); err != nil {
return nil, err
}
}
} else {
err := filepath.Walk(dirs.PkgDir, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
trimmed := strings.TrimPrefix(path, dirs.PkgDir)
return processPath(path, trimmed, false)
})
if err != nil {
return nil, err
}
}
return contents, nil
}
var RegexpALRPackageName = regexp.MustCompile(`^(?P<package>[^+]+)\+alr-(?P<repo>.+)$`)
func getBasePkgInfo(vars *types.BuildVars, input interface {
RepositoryProvider
OsInfoProvider
},
) *nfpm.Info {
return &nfpm.Info{
Name: fmt.Sprintf("%s+alr-%s", vars.Name, input.Repository()),
Arch: cpu.Arch(),
Version: vars.Version,
Release: overrides.ReleasePlatformSpecific(vars.Release, input.OSRelease()),
Epoch: strconv.FormatUint(uint64(vars.Epoch), 10),
}
}
// Функция getPkgFormat возвращает формат пакета из менеджера пакетов,
// или ALR_PKG_FORMAT, если он установлен.
func GetPkgFormat(mgr manager.Manager) string {
pkgFormat := mgr.Format()
if format, ok := os.LookupEnv("ALR_PKG_FORMAT"); ok {
pkgFormat = format
}
return pkgFormat
}
// Функция createBuildEnvVars создает переменные окружения, которые будут установлены
// в скрипте сборки при его выполнении.
func createBuildEnvVars(info *distro.OSRelease, dirs types.Directories) []string {
env := os.Environ()
env = append(
env,
"DISTRO_NAME="+info.Name,
"DISTRO_PRETTY_NAME="+info.PrettyName,
"DISTRO_ID="+info.ID,
"DISTRO_VERSION_ID="+info.VersionID,
"DISTRO_ID_LIKE="+strings.Join(info.Like, " "),
"ARCH="+cpu.Arch(),
"NCPU="+strconv.Itoa(runtime.NumCPU()),
)
if dirs.ScriptDir != "" {
env = append(env, "scriptdir="+dirs.ScriptDir)
}
if dirs.PkgDir != "" {
env = append(env, "pkgdir="+dirs.PkgDir)
}
if dirs.SrcDir != "" {
env = append(env, "srcdir="+dirs.SrcDir)
}
return env
}
// Функция setScripts добавляет скрипты-перехватчики к метаданным пакета.
func setScripts(vars *types.BuildVars, info *nfpm.Info, scriptDir string) {
if vars.Scripts.PreInstall != "" {
info.Scripts.PreInstall = filepath.Join(scriptDir, vars.Scripts.PreInstall)
}
if vars.Scripts.PostInstall != "" {
info.Scripts.PostInstall = filepath.Join(scriptDir, vars.Scripts.PostInstall)
}
if vars.Scripts.PreRemove != "" {
info.Scripts.PreRemove = filepath.Join(scriptDir, vars.Scripts.PreRemove)
}
if vars.Scripts.PostRemove != "" {
info.Scripts.PostRemove = filepath.Join(scriptDir, vars.Scripts.PostRemove)
}
if vars.Scripts.PreUpgrade != "" {
info.ArchLinux.Scripts.PreUpgrade = filepath.Join(scriptDir, vars.Scripts.PreUpgrade)
info.APK.Scripts.PreUpgrade = filepath.Join(scriptDir, vars.Scripts.PreUpgrade)
}
if vars.Scripts.PostUpgrade != "" {
info.ArchLinux.Scripts.PostUpgrade = filepath.Join(scriptDir, vars.Scripts.PostUpgrade)
info.APK.Scripts.PostUpgrade = filepath.Join(scriptDir, vars.Scripts.PostUpgrade)
}
if vars.Scripts.PreTrans != "" {
info.RPM.Scripts.PreTrans = filepath.Join(scriptDir, vars.Scripts.PreTrans)
}
if vars.Scripts.PostTrans != "" {
info.RPM.Scripts.PostTrans = filepath.Join(scriptDir, vars.Scripts.PostTrans)
}
}
/*
// Функция setVersion изменяет переменную версии в скрипте runner.
// Она используется для установки версии на вывод функции version().
func setVersion(ctx context.Context, r *interp.Runner, to string) error {
fl, err := syntax.NewParser().Parse(strings.NewReader("version='"+to+"'"), "")
if err != nil {
return err
}
return r.Run(ctx, fl)
}
*/
// Функция packageNames возвращает имена всех предоставленных пакетов.
/*
func packageNames(pkgs []db.Package) []string {
names := make([]string, len(pkgs))
for i, p := range pkgs {
names[i] = p.Name
}
return names
}
*/
// Функция removeDuplicates убирает любые дубликаты из предоставленного среза.
func removeDuplicates[T comparable](slice []T) []T {
seen := map[T]struct{}{}
result := []T{}
for _, item := range slice {
if _, ok := seen[item]; !ok {
seen[item] = struct{}{}
result = append(result, item)
}
}
return result
}
func removeDuplicatesSources(sources, checksums []string) ([]string, []string) {
seen := map[string]string{}
keys := make([]string, 0)
for i, s := range sources {
if val, ok := seen[s]; !ok || strings.EqualFold(val, "SKIP") {
if !ok {
keys = append(keys, s)
}
seen[s] = checksums[i]
}
}
newSources := make([]string, len(keys))
newChecksums := make([]string, len(keys))
for i, k := range keys {
newSources[i] = k
newChecksums[i] = seen[k]
}
return newSources, newChecksums
}

View File

@ -1,47 +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 (
"testing"
"github.com/stretchr/testify/assert"
)
func TestRemoveDuplicatesSources(t *testing.T) {
type testCase struct {
Name string
Sources []string
Checksums []string
NewSources []string
NewChecksums []string
}
for _, tc := range []testCase{{
Name: "prefer non-skip values",
Sources: []string{"a", "b", "c", "a"},
Checksums: []string{"skip", "skip", "skip", "1"},
NewSources: []string{"a", "b", "c"},
NewChecksums: []string{"1", "skip", "skip"},
}} {
t.Run(tc.Name, func(t *testing.T) {
s, c := removeDuplicatesSources(tc.Sources, tc.Checksums)
assert.Equal(t, s, tc.NewSources)
assert.Equal(t, c, tc.NewChecksums)
})
}
}

View File

@ -1,123 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 distro
import (
"context"
"os"
"strings"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/handlers"
)
// OSRelease contains information from an os-release file
type OSRelease struct {
Name string
PrettyName string
ID string
Like []string
VersionID string
ANSIColor string
HomeURL string
DocumentationURL string
SupportURL string
BugReportURL string
Logo string
PlatformID string
}
var parsed *OSRelease
// OSReleaseName returns a struct parsed from the system's os-release
// file. It checks /etc/os-release as well as /usr/lib/os-release.
// The first time it's called, it'll parse the os-release file.
// Subsequent calls will return the same value.
func ParseOSRelease(ctx context.Context) (*OSRelease, error) {
if parsed != nil {
return parsed, nil
}
fl, err := os.Open("/usr/lib/os-release")
if err != nil {
fl, err = os.Open("/etc/os-release")
if err != nil {
return nil, err
}
}
file, err := syntax.NewParser().Parse(fl, "/usr/lib/os-release")
if err != nil {
return nil, err
}
fl.Close()
// Create new shell interpreter with nop open, exec, readdir, and stat handlers
// as well as no environment variables in order to prevent vulnerabilities
// caused by changing the os-release file.
runner, err := interp.New(
interp.OpenHandler(handlers.NopOpen),
interp.ExecHandler(handlers.NopExec),
interp.ReadDirHandler2(handlers.NopReadDir),
interp.StatHandler(handlers.NopStat),
interp.Env(expand.ListEnviron()),
interp.Dir("/"),
)
if err != nil {
return nil, err
}
err = runner.Run(ctx, file)
if err != nil {
return nil, err
}
out := &OSRelease{
Name: runner.Vars["NAME"].Str,
PrettyName: runner.Vars["PRETTY_NAME"].Str,
ID: runner.Vars["ID"].Str,
VersionID: runner.Vars["VERSION_ID"].Str,
ANSIColor: runner.Vars["ANSI_COLOR"].Str,
HomeURL: runner.Vars["HOME_URL"].Str,
DocumentationURL: runner.Vars["DOCUMENTATION_URL"].Str,
SupportURL: runner.Vars["SUPPORT_URL"].Str,
BugReportURL: runner.Vars["BUG_REPORT_URL"].Str,
Logo: runner.Vars["LOGO"].Str,
PlatformID: runner.Vars["PLATFORM_ID"].Str,
}
distroUpdated := false
if distID, ok := os.LookupEnv("ALR_DISTRO"); ok {
out.ID = distID
}
if distLike, ok := os.LookupEnv("ALR_DISTRO_LIKE"); ok {
out.Like = strings.Split(distLike, " ")
} else if runner.Vars["ID_LIKE"].IsSet() && !distroUpdated {
out.Like = strings.Split(runner.Vars["ID_LIKE"].Str, " ")
}
parsed = out
return out, nil
}

View File

@ -1,39 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 gen
import (
"strings"
"text/template"
)
// Определяем переменную funcs типа template.FuncMap, которая будет использоваться для
// предоставления пользовательских функций в шаблонах
var funcs = template.FuncMap{
// Функция "tolower" использует strings.ToLower
// для преобразования строки в нижний регистр
"tolower": strings.ToLower,
// Функция "firstchar" — это лямбда-функция, которая берет строку
// и возвращает её первый символ
"firstchar": func(s string) string {
return s[:1]
},
}

View File

@ -1,118 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 gen
import (
_ "embed" // Пакет для встраивания содержимого файлов в бинарники Go, использовав откладку //go:embed
"encoding/json" // Пакет для работы с JSON: декодирование и кодирование
"errors" // Пакет для создания и обработки ошибок
"fmt" // Пакет для форматированного ввода и вывода
"io" // Пакет для интерфейсов ввода и вывода
"net/http" // Пакет для HTTP-клиентов и серверов
"text/template" // Пакет для обработки текстовых шаблонов
)
// Используем директиву //go:embed для встраивания содержимого файла шаблона в строку pipTmpl
// Встраивание файла tmpls/pip.tmpl.sh
//
//go:embed tmpls/pip.tmpl.sh
var pipTmpl string
// PipOptions содержит параметры, которые будут переданы в шаблон
type PipOptions struct {
Name string // Имя пакета
Version string // Версия пакета
Description string // Описание пакета
}
// pypiAPIResponse представляет структуру ответа от API PyPI
type pypiAPIResponse struct {
Info pypiInfo `json:"info"` // Информация о пакете
URLs []pypiURL `json:"urls"` // Список URL-адресов для загрузки пакета
}
// Метод SourceURL ищет и возвращает URL исходного distribution для пакета, если он существует
func (res pypiAPIResponse) SourceURL() (pypiURL, error) {
for _, url := range res.URLs {
if url.PackageType == "sdist" {
return url, nil
}
}
return pypiURL{}, errors.New("package doesn't have a source distribution")
}
// pypiInfo содержит основную информацию о пакете, такую как имя, версия и пр.
type pypiInfo struct {
Name string `json:"name"`
Version string `json:"version"`
Summary string `json:"summary"`
Homepage string `json:"home_page"`
License string `json:"license"`
}
// pypiURL представляет информацию об одном из доступных для загрузки URL
type pypiURL struct {
Digests map[string]string `json:"digests"` // Контрольные суммы для файлов
Filename string `json:"filename"` // Имя файла
PackageType string `json:"packagetype"` // Тип пакета (например sdist)
}
// Функция Pip загружает информацию о пакете из PyPI и использует шаблон для вывода информации
func Pip(w io.Writer, opts PipOptions) error {
// Создаем новый шаблон с добавлением функций из FuncMap
tmpl, err := template.New("pip").
Funcs(funcs).
Parse(pipTmpl)
if err != nil {
return err
}
// Формируем URL для запроса к PyPI на основании имени и версии пакета
url := fmt.Sprintf(
"https://pypi.org/pypi/%s/%s/json",
opts.Name,
opts.Version,
)
// Выполняем HTTP GET запрос к PyPI
res, err := http.Get(url)
if err != nil {
return err
}
defer res.Body.Close() // Закрываем тело ответа после завершения работы
if res.StatusCode != 200 {
return fmt.Errorf("pypi: %s", res.Status)
}
// Раскодируем ответ JSON от PyPI в структуру pypiAPIResponse
var resp pypiAPIResponse
err = json.NewDecoder(res.Body).Decode(&resp)
if err != nil {
return err
}
// Если в opts указано описание, используем его вместо описания из PyPI
if opts.Description != "" {
resp.Info.Summary = opts.Description
}
// Выполняем шаблон с использованием данных из resp и записываем результат в w
return tmpl.Execute(w, resp)
}

View File

@ -1,55 +0,0 @@
# This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
# It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
#
# 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/>.
name='python3-{{.Info.Name | tolower}}'
version='{{.Info.Version}}'
release='1'
desc='{{.Info.Summary}}'
homepage='{{.Info.Homepage}}'
maintainer='Example <user@example.com>'
architectures=('all')
license=('{{if .Info.License | ne ""}}{{.Info.License}}{{else}}custom:Unknown{{end}}')
provides=('{{.Info.Name | tolower}}')
conflicts=('{{.Info.Name | tolower}}')
deps=("python3")
deps_arch=("python")
deps_alpine=("python3")
build_deps=("python3" "python3-pip")
build_deps_arch=("python" "python-pip")
build_deps_alpine=("python3" "py3-pip")
sources=("https://files.pythonhosted.org/packages/source/{{.SourceURL.Filename | firstchar}}/{{.Info.Name}}/{{.SourceURL.Filename}}")
checksums=('blake2b-256:{{.SourceURL.Digests.blake2b_256}}')
build() {
cd "$srcdir/{{.Info.Name}}-${version}"
python -m build --wheel --no-isolation
}
package() {
cd "$srcdir/{{.Info.Name}}-${version}"
pip install --root="${pkgdir}/" . --no-deps --ignore-installed --disable-pip-version-check
}
files() {
printf '"%s" ' ./usr/local/lib/python3.*/site-packages/{{.Info.Name | tolower}}/*
printf '"%s" ' ./usr/local/lib/python3.*/site-packages/{{.Info.Name | tolower}}-${version}.dist-info/*
}

View File

@ -1,169 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 manager
import (
"bufio"
"fmt"
"os/exec"
"strings"
)
// APK represents the APK package manager
type APK struct {
CommonPackageManager
}
func NewAPK() *APK {
return &APK{
CommonPackageManager: CommonPackageManager{
noConfirmArg: "-i",
},
}
}
func (*APK) Exists() bool {
_, err := exec.LookPath("apk")
return err == nil
}
func (*APK) Name() string {
return "apk"
}
func (*APK) Format() string {
return "apk"
}
func (a *APK) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apk", "update")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("apk: sync: %w", err)
}
return nil
}
func (a *APK) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apk", "add")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("apk: install: %w", err)
}
return nil
}
func (a *APK) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apk", "add", "--allow-untrusted")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("apk: installlocal: %w", err)
}
return nil
}
func (a *APK) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apk", "del")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("apk: remove: %w", err)
}
return nil
}
func (a *APK) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apk", "upgrade")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("apk: upgrade: %w", err)
}
return nil
}
func (a *APK) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
return a.Upgrade(opts)
}
func (a *APK) ListInstalled(opts *Opts) (map[string]string, error) {
out := map[string]string{}
cmd := exec.Command("apk", "list", "-I")
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
err = cmd.Start()
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
name, info, ok := strings.Cut(scanner.Text(), "-")
if !ok {
continue
}
version, _, ok := strings.Cut(info, " ")
if !ok {
continue
}
out[name] = version
}
err = scanner.Err()
if err != nil {
return nil, err
}
return out, nil
}
func (a *APK) IsInstalled(pkg string) (bool, error) {
cmd := exec.Command("apk", "info", "--installed", pkg)
output, err := cmd.CombinedOutput()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
// Exit code 1 means the package is not installed
if exitErr.ExitCode() == 1 {
return false, nil
}
}
return false, fmt.Errorf("apk: isinstalled: %w, output: %s", err, output)
}
return true, nil
}

View File

@ -1,155 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 manager
import (
"bufio"
"fmt"
"os/exec"
"strings"
)
// APT represents the APT package manager
type APT struct {
CommonPackageManager
}
func NewAPT() *APT {
return &APT{
CommonPackageManager: CommonPackageManager{
noConfirmArg: "-y",
},
}
}
func (*APT) Exists() bool {
_, err := exec.LookPath("apt")
return err == nil
}
func (*APT) Name() string {
return "apt"
}
func (*APT) Format() string {
return "deb"
}
func (a *APT) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt", "update")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("apt: sync: %w", err)
}
return nil
}
func (a *APT) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt", "install")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("apt: install: %w", err)
}
return nil
}
func (a *APT) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return a.Install(opts, pkgs...)
}
func (a *APT) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt", "remove")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("apt: remove: %w", err)
}
return nil
}
func (a *APT) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return a.Install(opts, pkgs...)
}
func (a *APT) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt", "upgrade")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("apt: upgradeall: %w", err)
}
return nil
}
func (a *APT) ListInstalled(opts *Opts) (map[string]string, error) {
out := map[string]string{}
cmd := exec.Command("dpkg-query", "-f", "${Package}\u200b${Version}\\n", "-W")
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
err = cmd.Start()
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
name, version, ok := strings.Cut(scanner.Text(), "\u200b")
if !ok {
continue
}
out[name] = version
}
err = scanner.Err()
if err != nil {
return nil, err
}
return out, nil
}
func (a *APT) IsInstalled(pkg string) (bool, error) {
cmd := exec.Command("dpkg-query", "-l", pkg)
output, err := cmd.CombinedOutput()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
// Exit code 1 means the package is not installed
if exitErr.ExitCode() == 1 {
return false, nil
}
}
return false, fmt.Errorf("apt: isinstalled: %w, output: %s", err, output)
}
return true, nil
}

View File

@ -1,112 +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 manager
import (
"fmt"
"os/exec"
"strings"
)
// APTRpm represents the APT-RPM package manager
type APTRpm struct {
CommonPackageManager
CommonRPM
}
func NewAPTRpm() *APTRpm {
return &APTRpm{
CommonPackageManager: CommonPackageManager{
noConfirmArg: "-y",
},
}
}
func (*APTRpm) Name() string {
return "apt-rpm"
}
func (*APTRpm) Format() string {
return "rpm"
}
func (*APTRpm) Exists() bool {
cmd := exec.Command("apt-config", "dump")
output, err := cmd.Output()
if err != nil {
return false
}
return strings.Contains(string(output), "RPM")
}
func (a *APTRpm) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt-get", "update")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("apt-get: sync: %w", err)
}
return nil
}
func (a *APTRpm) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt-get", "install")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
cmd.Stdout = cmd.Stderr
err := cmd.Run()
if err != nil {
return fmt.Errorf("apt-get: install: %w", err)
}
return nil
}
func (a *APTRpm) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return a.Install(opts, pkgs...)
}
func (a *APTRpm) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt-get", "remove")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("apt-get: remove: %w", err)
}
return nil
}
func (a *APTRpm) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return a.Install(opts, pkgs...)
}
func (a *APTRpm) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
cmd := a.getCmd(opts, "apt-get", "dist-upgrade")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("apt-get: upgradeall: %w", err)
}
return nil
}

View File

@ -1,35 +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 manager
import "os/exec"
type CommonPackageManager struct {
noConfirmArg string
}
func (m *CommonPackageManager) getCmd(opts *Opts, mgrCmd string, args ...string) *exec.Cmd {
cmd := exec.Command(mgrCmd)
cmd.Args = append(cmd.Args, opts.Args...)
cmd.Args = append(cmd.Args, args...)
if opts.NoConfirm {
cmd.Args = append(cmd.Args, m.noConfirmArg)
}
return cmd
}

View File

@ -1,72 +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 manager
import (
"bufio"
"fmt"
"os/exec"
"strings"
)
type CommonRPM struct{}
func (c *CommonRPM) ListInstalled(opts *Opts) (map[string]string, error) {
out := map[string]string{}
cmd := exec.Command("rpm", "-qa", "--queryformat", "%{NAME}\u200b%|EPOCH?{%{EPOCH}:}:{}|%{VERSION}-%{RELEASE}\\n")
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
err = cmd.Start()
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
name, version, ok := strings.Cut(scanner.Text(), "\u200b")
if !ok {
continue
}
version = strings.TrimPrefix(version, "0:")
out[name] = version
}
err = scanner.Err()
if err != nil {
return nil, err
}
return out, nil
}
func (a *CommonRPM) IsInstalled(pkg string) (bool, error) {
cmd := exec.Command("rpm", "-q", "--whatprovides", pkg)
output, err := cmd.CombinedOutput()
if err != nil {
if exitErr, ok := err.(*exec.ExitError); ok {
if exitErr.ExitCode() == 1 {
return false, nil
}
}
return false, fmt.Errorf("rpm: isinstalled: %w, output: %s", err, output)
}
return true, nil
}

View File

@ -1,120 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 manager
import (
"fmt"
"os/exec"
)
type DNF struct {
CommonPackageManager
CommonRPM
}
func NewDNF() *DNF {
return &DNF{
CommonPackageManager: CommonPackageManager{
noConfirmArg: "-y",
},
}
}
func (*DNF) Exists() bool {
_, err := exec.LookPath("dnf")
return err == nil
}
func (*DNF) Name() string {
return "dnf"
}
func (*DNF) Format() string {
return "rpm"
}
// Sync выполняет upgrade всех установленных пакетов, обновляя их до более новых версий
func (d *DNF) Sync(opts *Opts) error {
opts = ensureOpts(opts) // Гарантирует, что opts не равен nil и содержит допустимые значения
cmd := d.getCmd(opts, "dnf", "upgrade")
setCmdEnv(cmd) // Устанавливает переменные окружения для команды
err := cmd.Run() // Выполняет команду
if err != nil {
return fmt.Errorf("dnf: sync: %w", err)
}
return nil
}
// Install устанавливает указанные пакеты с помощью DNF
func (d *DNF) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := d.getCmd(opts, "dnf", "install", "--allowerasing")
cmd.Args = append(cmd.Args, pkgs...) // Добавляем названия пакетов к команде
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("dnf: install: %w", err)
}
return nil
}
// InstallLocal расширяет метод Install для установки пакетов, расположенных локально
func (d *DNF) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return d.Install(opts, pkgs...)
}
// Remove удаляет указанные пакеты с помощью DNF
func (d *DNF) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := d.getCmd(opts, "dnf", "remove")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("dnf: remove: %w", err)
}
return nil
}
// Upgrade обновляет указанные пакеты до более новых версий
func (d *DNF) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := d.getCmd(opts, "dnf", "upgrade")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("dnf: upgrade: %w", err)
}
return nil
}
// UpgradeAll обновляет все установленные пакеты
func (d *DNF) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
cmd := d.getCmd(opts, "dnf", "upgrade")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("dnf: upgradeall: %w", err)
}
return nil
}

View File

@ -1,114 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 manager
import (
"os"
"os/exec"
)
var Args []string
type Opts struct {
NoConfirm bool
Args []string
}
var DefaultOpts = &Opts{
NoConfirm: false,
}
var managers = []Manager{
NewPacman(),
NewAPT(),
NewDNF(),
NewYUM(),
NewAPK(),
NewZypper(),
NewAPTRpm(),
}
// Register registers a new package manager
func Register(m Manager) {
managers = append(managers, m)
}
// Manager represents a system package manager
type Manager interface {
// Name returns the name of the manager.
Name() string
// Format returns the packaging format of the manager.
// Examples: rpm, deb, apk
Format() string
// Returns true if the package manager exists on the system.
Exists() bool
// Sync fetches repositories without installing anything
Sync(*Opts) error
// Install installs packages
Install(*Opts, ...string) error
// Remove uninstalls packages
Remove(*Opts, ...string) error
// Upgrade upgrades packages
Upgrade(*Opts, ...string) error
// InstallLocal installs packages from local files rather than repos
InstallLocal(*Opts, ...string) error
// UpgradeAll upgrades all packages
UpgradeAll(*Opts) error
// ListInstalled returns all installed packages mapped to their versions
ListInstalled(*Opts) (map[string]string, error)
//
IsInstalled(string) (bool, error)
}
// Detect returns the package manager detected on the system
func Detect() Manager {
for _, mgr := range managers {
if mgr.Exists() {
return mgr
}
}
return nil
}
// Get returns the package manager with the given name
func Get(name string) Manager {
for _, mgr := range managers {
if mgr.Name() == name {
return mgr
}
}
return nil
}
func setCmdEnv(cmd *exec.Cmd) {
cmd.Env = os.Environ()
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stderr
cmd.Stderr = os.Stderr
}
func ensureOpts(opts *Opts) *Opts {
if opts == nil {
opts = DefaultOpts
}
opts.Args = append(opts.Args, Args...)
return opts
}

View File

@ -1,162 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 manager
import (
"bufio"
"fmt"
"os/exec"
"strings"
)
// Pacman represents the Pacman package manager
type Pacman struct {
CommonPackageManager
}
func NewPacman() *Pacman {
return &Pacman{
CommonPackageManager: CommonPackageManager{
noConfirmArg: "--noconfirm",
},
}
}
func (*Pacman) Exists() bool {
_, err := exec.LookPath("pacman")
return err == nil
}
func (*Pacman) Name() string {
return "pacman"
}
func (*Pacman) Format() string {
return "archlinux"
}
func (p *Pacman) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := p.getCmd(opts, "pacman", "-Sy")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("pacman: sync: %w", err)
}
return nil
}
func (p *Pacman) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := p.getCmd(opts, "pacman", "-S", "--needed")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("pacman: install: %w", err)
}
return nil
}
func (p *Pacman) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := p.getCmd(opts, "pacman", "-U", "--needed")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("pacman: installlocal: %w", err)
}
return nil
}
func (p *Pacman) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := p.getCmd(opts, "pacman", "-R")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("pacman: remove: %w", err)
}
return nil
}
func (p *Pacman) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return p.Install(opts, pkgs...)
}
func (p *Pacman) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
cmd := p.getCmd(opts, "pacman", "-Su")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("pacman: upgradeall: %w", err)
}
return nil
}
func (p *Pacman) ListInstalled(opts *Opts) (map[string]string, error) {
out := map[string]string{}
cmd := exec.Command("pacman", "-Q")
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
err = cmd.Start()
if err != nil {
return nil, err
}
scanner := bufio.NewScanner(stdout)
for scanner.Scan() {
name, version, ok := strings.Cut(scanner.Text(), " ")
if !ok {
continue
}
out[name] = version
}
err = scanner.Err()
if err != nil {
return nil, err
}
return out, nil
}
func (p *Pacman) IsInstalled(pkg string) (bool, error) {
cmd := exec.Command("pacman", "-Q", pkg)
output, err := cmd.CombinedOutput()
if err != nil {
// Pacman returns exit code 1 if the package is not found
if exitErr, ok := err.(*exec.ExitError); ok {
if exitErr.ExitCode() == 1 {
return false, nil
}
}
return false, fmt.Errorf("pacman: isinstalled: %w, output: %s", err, output)
}
return true, nil
}

View File

@ -1,115 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 manager
import (
"fmt"
"os/exec"
)
// YUM represents the YUM package manager
type YUM struct {
CommonPackageManager
CommonRPM
}
func NewYUM() *YUM {
return &YUM{
CommonPackageManager: CommonPackageManager{
noConfirmArg: "-y",
},
}
}
func (*YUM) Exists() bool {
_, err := exec.LookPath("yum")
return err == nil
}
func (*YUM) Name() string {
return "yum"
}
func (*YUM) Format() string {
return "rpm"
}
func (y *YUM) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := y.getCmd(opts, "yum", "upgrade")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("yum: sync: %w", err)
}
return nil
}
func (y *YUM) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := y.getCmd(opts, "yum", "install", "--allowerasing")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("yum: install: %w", err)
}
return nil
}
func (y *YUM) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return y.Install(opts, pkgs...)
}
func (y *YUM) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := y.getCmd(opts, "yum", "remove")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("yum: remove: %w", err)
}
return nil
}
func (y *YUM) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := y.getCmd(opts, "yum", "upgrade")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("yum: upgrade: %w", err)
}
return nil
}
func (y *YUM) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
cmd := y.getCmd(opts, "yum", "upgrade")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("yum: upgradeall: %w", err)
}
return nil
}

View File

@ -1,115 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 manager
import (
"fmt"
"os/exec"
)
// Zypper represents the Zypper package manager
type Zypper struct {
CommonPackageManager
CommonRPM
}
func NewZypper() *YUM {
return &YUM{
CommonPackageManager: CommonPackageManager{
noConfirmArg: "-y",
},
}
}
func (*Zypper) Exists() bool {
_, err := exec.LookPath("zypper")
return err == nil
}
func (*Zypper) Name() string {
return "zypper"
}
func (*Zypper) Format() string {
return "rpm"
}
func (z *Zypper) Sync(opts *Opts) error {
opts = ensureOpts(opts)
cmd := z.getCmd(opts, "zypper", "refresh")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("zypper: sync: %w", err)
}
return nil
}
func (z *Zypper) Install(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := z.getCmd(opts, "zypper", "install", "-y")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("zypper: install: %w", err)
}
return nil
}
func (z *Zypper) InstallLocal(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
return z.Install(opts, pkgs...)
}
func (z *Zypper) Remove(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := z.getCmd(opts, "zypper", "remove", "-y")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("zypper: remove: %w", err)
}
return nil
}
func (z *Zypper) Upgrade(opts *Opts, pkgs ...string) error {
opts = ensureOpts(opts)
cmd := z.getCmd(opts, "zypper", "update", "-y")
cmd.Args = append(cmd.Args, pkgs...)
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("zypper: upgrade: %w", err)
}
return nil
}
func (z *Zypper) UpgradeAll(opts *Opts) error {
opts = ensureOpts(opts)
cmd := z.getCmd(opts, "zypper", "update", "-y")
setCmdEnv(cmd)
err := cmd.Run()
if err != nil {
return fmt.Errorf("zypper: upgradeall: %w", err)
}
return nil
}

View File

@ -1,38 +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 parser
import (
"fmt"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder"
)
type PackageNames struct {
BasePkgName string `sh:"basepkg_name"`
Names []string `sh:"name"`
}
func ParseNames(dec *decoder.Decoder) (*PackageNames, error) {
var pkgs PackageNames
err := dec.DecodeVars(&pkgs)
if err != nil {
return nil, fmt.Errorf("fail parse names: %w", err)
}
return &pkgs, nil
}

View File

@ -1,81 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 repos
import (
"context"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
)
func (rs *Repos) FindPkgs(ctx context.Context, pkgs []string) (map[string][]db.Package, []string, error) {
found := map[string][]db.Package{}
notFound := []string(nil)
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
}
added := 0
for result.Next() {
var pkg db.Package
err = result.StructScan(&pkg)
if err != nil {
return nil, nil, err
}
added++
found[pkgName] = append(found[pkgName], pkg)
}
result.Close()
if added == 0 {
result, err := rs.db.GetPkgs(ctx, "name LIKE ?", pkgName)
if err != nil {
return nil, nil, err
}
for result.Next() {
var pkg db.Package
err = result.StructScan(&pkg)
if err != nil {
return nil, nil, err
}
added++
found[pkgName] = append(found[pkgName], pkg)
}
result.Close()
}
if added == 0 {
notFound = append(notFound, pkgName)
}
}
return found, notFound, nil
}

View File

@ -1,147 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 repos_test
import (
"reflect"
"strings"
"testing"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/repos"
)
func TestFindPkgs(t *testing.T) {
e := prepare(t)
defer cleanup(t, e)
rs := repos.New(
e.Cfg,
e.Db,
)
err := rs.Pull(e.Ctx, []types.Repo{
{
Name: "default",
URL: "https://gitea.plemya-x.ru/Plemya-x/alr-default.git",
},
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
found, notFound, err := rs.FindPkgs(
e.Ctx,
[]string{"alr", "nonexistentpackage1", "nonexistentpackage2"},
)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
if !reflect.DeepEqual(notFound, []string{"nonexistentpackage1", "nonexistentpackage2"}) {
t.Errorf("Expected 'nonexistentpackage{1,2} not to be found")
}
if len(found) != 1 {
t.Errorf("Expected 1 package found, got %d", len(found))
}
alrPkgs, ok := found["alr"]
if !ok {
t.Fatalf("Expected 'alr' packages to be found")
}
if len(alrPkgs) < 2 {
t.Errorf("Expected two 'alr' packages to be found")
}
for i, pkg := range alrPkgs {
if !strings.HasPrefix(pkg.Name, "alr") {
t.Errorf("Expected package name of all found packages to start with 'alr', got %s on element %d", pkg.Name, i)
}
}
}
func TestFindPkgsEmpty(t *testing.T) {
e := prepare(t)
defer cleanup(t, e)
rs := repos.New(
e.Cfg,
e.Db,
)
err := e.Db.InsertPackage(e.Ctx, db.Package{
Name: "test1",
Repository: "default",
Version: "0.0.1",
Release: 1,
Description: db.NewJSON(map[string]string{
"en": "Test package 1",
"ru": "Проверочный пакет 1",
}),
Provides: db.NewJSON([]string{""}),
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
err = e.Db.InsertPackage(e.Ctx, db.Package{
Name: "test2",
Repository: "default",
Version: "0.0.1",
Release: 1,
Description: db.NewJSON(map[string]string{
"en": "Test package 2",
"ru": "Проверочный пакет 2",
}),
Provides: db.NewJSON([]string{"test"}),
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
found, notFound, err := rs.FindPkgs(e.Ctx, []string{"test", ""})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
if len(notFound) != 0 {
t.Errorf("Expected all packages to be found")
}
if len(found) != 1 {
t.Errorf("Expected 1 package found, got %d", len(found))
}
testPkgs, ok := found["test"]
if !ok {
t.Fatalf("Expected 'test' packages to be found")
}
if len(testPkgs) != 1 {
t.Errorf("Expected one 'test' package to be found, got %d", len(testPkgs))
}
if testPkgs[0].Name != "test2" {
t.Errorf("Expected 'test2' package, got '%s'", testPkgs[0].Name)
}
}

View File

@ -1,400 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 repos
import (
"context"
"errors"
"fmt"
"io"
"log/slog"
"net/url"
"os"
"path/filepath"
"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"
"github.com/leonelquinteros/gotext"
"github.com/pelletier/go-toml/v2"
"go.elara.ws/vercmp"
"mvdan.cc/sh/v3/expand"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/handlers"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
)
type actionType uint8
const (
actionDelete actionType = iota
actionUpdate
)
type action struct {
Type actionType
File string
}
// Pull pulls the provided repositories. If a repo doesn't exist, it will be cloned
// and its packages will be written to the DB. If it does exist, it will be pulled.
// In this case, only changed packages will be processed if possible.
// If repos is set to nil, the repos in the ALR config will be used.
func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error {
if repos == nil {
repos = rs.cfg.Repos()
}
for _, repo := range repos {
repoURL, err := url.Parse(repo.URL)
if err != nil {
return err
}
slog.Info(gotext.Get("Pulling repository"), "name", repo.Name)
repoDir := filepath.Join(rs.cfg.GetPaths().RepoDir, repo.Name)
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
}
err = r.FetchContext(ctx, &git.FetchOptions{
Progress: os.Stderr,
Force: true,
})
if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) {
return err
}
w, err := r.Worktree()
if err != nil {
return err
}
old, err := r.Head()
if err != nil {
return err
}
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(ctx) {
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)
}
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)
continue
}
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)
}
}
}
return nil
}
func (rs *Repos) updatePkg(ctx context.Context, repo types.Repo, runner *interp.Runner, scriptFl io.ReadCloser) error {
parser := syntax.NewParser()
pkgs, err := parseScript(ctx, repo, parser, runner, scriptFl)
if err != nil {
return err
}
for _, pkg := range pkgs {
err = rs.db.InsertPackage(ctx, *pkg)
if err != nil {
return err
}
}
return nil
}
func (rs *Repos) processRepoChangesRunner(repoDir, scriptDir string) (*interp.Runner, error) {
env := append(os.Environ(), "scriptdir="+scriptDir)
return interp.New(
interp.Env(expand.ListEnviron(env...)),
interp.ExecHandler(handlers.NopExec),
interp.ReadDirHandler2(handlers.RestrictedReadDir(repoDir)),
interp.StatHandler(handlers.RestrictedStat(repoDir)),
interp.OpenHandler(handlers.RestrictedOpen(repoDir)),
interp.StdIO(handlers.NopRWC{}, handlers.NopRWC{}, handlers.NopRWC{}),
// Use temp dir instead script dir because runner may be for deleted file
interp.Dir(os.TempDir()),
)
}
func (rs *Repos) processRepoChanges(ctx context.Context, repo types.Repo, r *git.Repository, w *git.Worktree, old, new *plumbing.Reference) error {
oldCommit, err := r.CommitObject(old.Hash())
if err != nil {
return err
}
newCommit, err := r.CommitObject(new.Hash())
if err != nil {
return err
}
patch, err := oldCommit.Patch(newCommit)
if err != nil {
return fmt.Errorf("error to create patch: %w", err)
}
var actions []action
for _, fp := range patch.FilePatches() {
from, to := fp.Files()
if !isValid(from, to) {
continue
}
switch {
case to == nil:
actions = append(actions, action{
Type: actionDelete,
File: from.Path(),
})
case from == nil:
actions = append(actions, action{
Type: actionUpdate,
File: to.Path(),
})
case from.Path() != to.Path():
actions = append(actions,
action{
Type: actionDelete,
File: from.Path(),
},
action{
Type: actionUpdate,
File: to.Path(),
},
)
default:
slog.Debug("unexpected, but I'll try to do")
actions = append(actions, action{
Type: actionUpdate,
File: to.Path(),
})
}
}
repoDir := w.Filesystem.Root()
parser := syntax.NewParser()
for _, action := range actions {
runner, err := rs.processRepoChangesRunner(repoDir, filepath.Dir(filepath.Join(repoDir, action.File)))
if err != nil {
return fmt.Errorf("error creating process repo changes runner: %w", err)
}
switch action.Type {
case actionDelete:
if filepath.Base(action.File) != "alr.sh" {
continue
}
scriptFl, err := oldCommit.File(action.File)
if err != nil {
return nil
}
r, err := scriptFl.Reader()
if err != nil {
return nil
}
pkgs, err := parseScript(ctx, repo, parser, runner, r)
if err != nil {
return err
}
for _, pkg := range pkgs {
err = rs.db.DeletePkgs(ctx, "name = ? AND repository = ?", pkg.Name, repo.Name)
if err != nil {
return err
}
}
case actionUpdate:
if filepath.Base(action.File) != "alr.sh" {
action.File = filepath.Join(filepath.Dir(action.File), "alr.sh")
}
scriptFl, err := newCommit.File(action.File)
if err != nil {
return nil
}
r, err := scriptFl.Reader()
if err != nil {
return nil
}
err = rs.updatePkg(ctx, repo, runner, r)
if err != nil {
return fmt.Errorf("error updatePkg: %w", err)
}
}
}
return nil
}
func (rs *Repos) processRepoFull(ctx context.Context, repo types.Repo, repoDir string) error {
glob := filepath.Join(repoDir, "/*/alr.sh")
matches, err := filepath.Glob(glob)
if err != nil {
return err
}
for _, match := range matches {
runner, err := rs.processRepoChangesRunner(repoDir, filepath.Dir(match))
if err != nil {
return err
}
scriptFl, err := os.Open(match)
if err != nil {
return err
}
err = rs.updatePkg(ctx, repo, runner, scriptFl)
if err != nil {
return err
}
}
return nil
}

View File

@ -1,173 +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 repos
import (
"context"
"io"
"os"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
)
type TestALRConfig struct{}
func (c *TestALRConfig) GetPaths() *config.Paths {
return &config.Paths{
DBPath: ":memory:",
}
}
func (c *TestALRConfig) Repos() []types.Repo {
return []types.Repo{
{
Name: "test",
URL: "https://test",
},
}
}
func createReadCloserFromString(input string) io.ReadCloser {
reader := strings.NewReader(input)
return struct {
io.Reader
io.Closer
}{
Reader: reader,
Closer: io.NopCloser(reader),
}
}
func TestUpdatePkg(t *testing.T) {
type testCase struct {
name string
file string
verify func(context.Context, *db.Database)
}
repo := types.Repo{
Name: "test",
URL: "https://test",
}
for _, tc := range []testCase{
{
name: "single package",
file: `name=foo
version='0.0.1'
release=1
desc="main desc"
deps=('sudo')
build_deps=('golang')
`,
verify: func(ctx context.Context, database *db.Database) {
result, err := database.GetPkgs(ctx, "1 = 1")
assert.NoError(t, err)
pkgCount := 0
for result.Next() {
var dbPkg db.Package
err = result.StructScan(&dbPkg)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
assert.Equal(t, "foo", dbPkg.Name)
assert.Equal(t, db.NewJSON(map[string]string{"": "main desc"}), dbPkg.Description)
assert.Equal(t, db.NewJSON(map[string][]string{"": {"sudo"}}), dbPkg.Depends)
pkgCount++
}
assert.Equal(t, 1, pkgCount)
},
},
{
name: "multiple package",
file: `basepkg_name=foo
name=(
bar
buz
)
version='0.0.1'
release=1
desc="main desc"
deps=('sudo')
build_deps=('golang')
meta_bar() {
desc="foo desc"
}
meta_buz() {
deps+=('doas')
}
`,
verify: func(ctx context.Context, database *db.Database) {
result, err := database.GetPkgs(ctx, "1 = 1")
assert.NoError(t, err)
pkgCount := 0
for result.Next() {
var dbPkg db.Package
err = result.StructScan(&dbPkg)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
if dbPkg.Name == "bar" {
assert.Equal(t, db.NewJSON(map[string]string{"": "foo desc"}), dbPkg.Description)
assert.Equal(t, db.NewJSON(map[string][]string{"": {"sudo"}}), dbPkg.Depends)
}
if dbPkg.Name == "buz" {
assert.Equal(t, db.NewJSON(map[string]string{"": "main desc"}), dbPkg.Description)
assert.Equal(t, db.NewJSON(map[string][]string{"": {"sudo", "doas"}}), dbPkg.Depends)
}
pkgCount++
}
assert.Equal(t, 2, pkgCount)
},
},
} {
t.Run(tc.name, func(t *testing.T) {
cfg := &TestALRConfig{}
ctx := context.Background()
database := db.New(&TestALRConfig{})
database.Init(ctx)
rs := New(cfg, database)
path, err := os.MkdirTemp("", "test-update-pkg")
assert.NoError(t, err)
defer os.RemoveAll(path)
runner, err := rs.processRepoChangesRunner(path, path)
assert.NoError(t, err)
err = rs.updatePkg(ctx, repo, runner, createReadCloserFromString(
tc.file,
))
assert.NoError(t, err)
tc.verify(ctx, database)
})
}
}

View File

@ -1,145 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 repos_test
import (
"context"
"os"
"path/filepath"
"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/types"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/repos"
)
type TestEnv struct {
Ctx context.Context
Cfg *TestALRConfig
Db *db.Database
}
type TestALRConfig struct {
CacheDir string
RepoDir string
PkgsDir string
}
func (c *TestALRConfig) GetPaths() *config.Paths {
return &config.Paths{
DBPath: ":memory:",
CacheDir: c.CacheDir,
RepoDir: c.RepoDir,
PkgsDir: c.PkgsDir,
}
}
func (c *TestALRConfig) Repos() []types.Repo {
return []types.Repo{}
}
func prepare(t *testing.T) *TestEnv {
t.Helper()
cacheDir, err := os.MkdirTemp("/tmp", "alr-pull-test.*")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
repoDir := filepath.Join(cacheDir, "repo")
err = os.MkdirAll(repoDir, 0o755)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
pkgsDir := filepath.Join(cacheDir, "pkgs")
err = os.MkdirAll(pkgsDir, 0o755)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
cfg := &TestALRConfig{
CacheDir: cacheDir,
RepoDir: repoDir,
PkgsDir: pkgsDir,
}
ctx := context.Background()
db := database.New(cfg)
db.Init(ctx)
return &TestEnv{
Cfg: cfg,
Db: db,
Ctx: ctx,
}
}
func cleanup(t *testing.T, e *TestEnv) {
t.Helper()
err := os.RemoveAll(e.Cfg.CacheDir)
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
e.Db.Close()
}
func TestPull(t *testing.T) {
e := prepare(t)
defer cleanup(t, e)
rs := repos.New(
e.Cfg,
e.Db,
)
err := rs.Pull(e.Ctx, []types.Repo{
{
Name: "default",
URL: "https://gitea.plemya-x.ru/Plemya-x/xpamych-alr-repo.git",
},
})
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
result, err := e.Db.GetPkgs(e.Ctx, "true")
if err != nil {
t.Fatalf("Expected no error, got %s", err)
}
var pkgAmt int
for result.Next() {
var dbPkg db.Package
err = result.StructScan(&dbPkg)
if err != nil {
t.Errorf("Expected no error, got %s", err)
}
pkgAmt++
}
if pkgAmt == 0 {
t.Errorf("Expected at least 1 matching package, but got %d", pkgAmt)
}
}

View File

@ -1,43 +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 repos
import (
"gitea.plemya-x.ru/Plemya-x/ALR/internal/config"
database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
)
type Config interface {
GetPaths() *config.Paths
Repos() []types.Repo
}
type Repos struct {
cfg Config
db *database.Database
}
func New(
cfg Config,
db *database.Database,
) *Repos {
return &Repos{
cfg,
db,
}
}

View File

@ -1,266 +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 repos
import (
"context"
"errors"
"fmt"
"io"
"path/filepath"
"reflect"
"strings"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/plumbing/format/diff"
"github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/plumbing/transport/client"
"mvdan.cc/sh/v3/interp"
"mvdan.cc/sh/v3/syntax"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/types"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/parser"
)
// isValid makes sure the path of the file being updated is valid.
// It checks to make sure the file is not within a nested directory
// and that it is called alr.sh.
func isValid(from, to diff.File) bool {
var path string
if from != nil {
path = from.Path()
}
if to != nil {
path = to.Path()
}
match, _ := filepath.Match("*/*.sh", path)
return match
}
func parseScript(
ctx context.Context,
repo types.Repo,
syntaxParser *syntax.Parser,
runner *interp.Runner,
r io.ReadCloser,
) ([]*db.Package, error) {
fl, err := syntaxParser.Parse(r, "alr.sh")
if err != nil {
return nil, err
}
runner.Reset()
err = runner.Run(ctx, fl)
if err != nil {
return nil, err
}
d := decoder.New(&distro.OSRelease{}, runner)
d.Overrides = false
d.LikeDistros = false
pkgNames, err := parser.ParseNames(d)
if err != nil {
return nil, fmt.Errorf("failed parsing package names: %w", err)
}
if len(pkgNames.Names) == 0 {
return nil, errors.New("package name is missing")
}
var dbPkgs []*db.Package
if len(pkgNames.Names) > 1 {
if pkgNames.BasePkgName == "" {
pkgNames.BasePkgName = pkgNames.Names[0]
}
for _, pkgName := range pkgNames.Names {
pkgInfo := PackageInfo{}
funcName := fmt.Sprintf("meta_%s", pkgName)
runner.Reset()
err = runner.Run(ctx, fl)
if err != nil {
return nil, err
}
meta, ok := d.GetFuncWithSubshell(funcName)
if !ok {
return nil, fmt.Errorf("func %s is missing", funcName)
}
r, err := meta(ctx)
if err != nil {
return nil, err
}
d := decoder.New(&distro.OSRelease{}, r)
d.Overrides = false
d.LikeDistros = false
err = d.DecodeVars(&pkgInfo)
if err != nil {
return nil, err
}
pkg := pkgInfo.ToPackage(repo.Name)
resolveOverrides(r, pkg)
pkg.Name = pkgName
pkg.BasePkgName = pkgNames.BasePkgName
dbPkgs = append(dbPkgs, pkg)
}
return dbPkgs, nil
}
pkg := EmptyPackage(repo.Name)
err = d.DecodeVars(pkg)
if err != nil {
return nil, err
}
resolveOverrides(runner, pkg)
dbPkgs = append(dbPkgs, pkg)
return dbPkgs, nil
}
type PackageInfo struct {
Version string `sh:"version,required"`
Release int `sh:"release,required"`
Epoch uint `sh:"epoch"`
Architectures db.JSON[[]string] `sh:"architectures"`
Licenses db.JSON[[]string] `sh:"license"`
Provides db.JSON[[]string] `sh:"provides"`
Conflicts db.JSON[[]string] `sh:"conflicts"`
Replaces db.JSON[[]string] `sh:"replaces"`
}
func (inf *PackageInfo) ToPackage(repoName string) *db.Package {
pkg := EmptyPackage(repoName)
pkg.Version = inf.Version
pkg.Release = inf.Release
pkg.Epoch = inf.Epoch
pkg.Architectures = inf.Architectures
pkg.Licenses = inf.Licenses
pkg.Provides = inf.Provides
pkg.Conflicts = inf.Conflicts
pkg.Replaces = inf.Replaces
return pkg
}
func EmptyPackage(repoName string) *db.Package {
return &db.Package{
Group: db.NewJSON(map[string]string{}),
Summary: db.NewJSON(map[string]string{}),
Description: db.NewJSON(map[string]string{}),
Homepage: db.NewJSON(map[string]string{}),
Maintainer: db.NewJSON(map[string]string{}),
Depends: db.NewJSON(map[string][]string{}),
BuildDepends: db.NewJSON(map[string][]string{}),
Repository: repoName,
}
}
var overridable = map[string]string{
"deps": "Depends",
"build_deps": "BuildDepends",
"desc": "Description",
"homepage": "Homepage",
"maintainer": "Maintainer",
"group": "Group",
"summary": "Summary",
}
func resolveOverrides(runner *interp.Runner, pkg *db.Package) {
pkgVal := reflect.ValueOf(pkg).Elem()
for name, val := range runner.Vars {
for prefix, field := range overridable {
if strings.HasPrefix(name, prefix) {
override := strings.TrimPrefix(name, prefix)
override = strings.TrimPrefix(override, "_")
field := pkgVal.FieldByName(field)
varVal := field.FieldByName("Val")
varType := varVal.Type()
switch varType.Elem().String() {
case "[]string":
varVal.SetMapIndex(reflect.ValueOf(override), reflect.ValueOf(val.List))
case "string":
varVal.SetMapIndex(reflect.ValueOf(override), reflect.ValueOf(val.Str))
}
break
}
}
}
}
func getHeadReference(r *git.Repository) (plumbing.ReferenceName, error) {
remote, err := r.Remote(git.DefaultRemoteName)
if err != nil {
return "", err
}
endpoint, err := transport.NewEndpoint(remote.Config().URLs[0])
if err != nil {
return "", err
}
gitClient, err := client.NewClient(endpoint)
if err != nil {
return "", err
}
session, err := gitClient.NewUploadPackSession(endpoint, nil)
if err != nil {
return "", err
}
info, err := session.AdvertisedReferences()
if err != nil {
return "", err
}
refs, err := info.AllReferences()
if err != nil {
return "", err
}
return refs["HEAD"].Target(), nil
}
func resolveHash(r *git.Repository, ref string) (*plumbing.Hash, error) {
var err error
if ref == "" {
reference, err := getHeadReference(r)
if err != nil {
return nil, fmt.Errorf("failed to get head reference %w", err)
}
ref = reference.Short()
}
hsh, err := r.ResolveRevision(git.DefaultRemoteName + "/" + plumbing.Revision(ref))
if err != nil {
hsh, err = r.ResolveRevision(plumbing.Revision(ref))
if err != nil {
return nil, err
}
}
return hsh, nil
}

View File

@ -1,66 +0,0 @@
// This file was originally part of the project "LURE - Linux User REpository", created by Elara Musayelyan.
// It has been modified as part of "ALR - Any Linux Repository" by the ALR Authors.
//
// 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 search
import (
"context"
"github.com/jmoiron/sqlx"
database "gitea.plemya-x.ru/Plemya-x/ALR/internal/db"
)
type PackagesProvider interface {
GetPkgs(ctx context.Context, where string, args ...any) (*sqlx.Rows, error)
}
type Searcher struct {
pp PackagesProvider
}
func New(pp PackagesProvider) *Searcher {
return &Searcher{
pp: pp,
}
}
func (s *Searcher) Search(
ctx context.Context,
opts *SearchOptions,
) ([]database.Package, error) {
var packages []database.Package
where, args := opts.WhereClause()
result, err := s.pp.GetPkgs(ctx, where, args...)
if err != nil {
return nil, err
}
for result.Next() {
var dbPkg database.Package
err = result.StructScan(&dbPkg)
if err != nil {
return nil, err
}
packages = append(packages, dbPkg)
}
return packages, nil
}

View File

@ -1,86 +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 search
import (
"fmt"
"strings"
)
type SearchOptions struct {
conditions []string
args []any
}
func (o *SearchOptions) WhereClause() (string, []any) {
if len(o.conditions) == 0 {
return "", nil
}
return strings.Join(o.conditions, " AND "), o.args
}
type SearchOptionsBuilder struct {
options SearchOptions
}
func NewSearchOptions() *SearchOptionsBuilder {
return &SearchOptionsBuilder{}
}
func (b *SearchOptionsBuilder) withGeneralLike(key, value string) *SearchOptionsBuilder {
if value != "" {
b.options.conditions = append(b.options.conditions, fmt.Sprintf("%s LIKE ?", key))
b.options.args = append(b.options.args, "%"+value+"%")
}
return b
}
func (b *SearchOptionsBuilder) withGeneralEqual(key string, value any) *SearchOptionsBuilder {
if value != "" {
b.options.conditions = append(b.options.conditions, fmt.Sprintf("%s = ?", key))
b.options.args = append(b.options.args, value)
}
return b
}
func (b *SearchOptionsBuilder) withGeneralJsonArrayContains(key string, value any) *SearchOptionsBuilder {
if value != "" {
b.options.conditions = append(b.options.conditions, fmt.Sprintf("json_array_contains(%s, ?)", key))
b.options.args = append(b.options.args, value)
}
return b
}
func (b *SearchOptionsBuilder) WithName(name string) *SearchOptionsBuilder {
return b.withGeneralLike("name", name)
}
func (b *SearchOptionsBuilder) WithDescription(description string) *SearchOptionsBuilder {
return b.withGeneralLike("description", description)
}
func (b *SearchOptionsBuilder) WithRepository(repository string) *SearchOptionsBuilder {
return b.withGeneralEqual("repository", repository)
}
func (b *SearchOptionsBuilder) WithProvides(provides string) *SearchOptionsBuilder {
return b.withGeneralJsonArrayContains("provides", provides)
}
func (b *SearchOptionsBuilder) Build() *SearchOptions {
return &b.options
}

View File

@ -1,65 +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 search_test
import (
"testing"
"github.com/stretchr/testify/assert"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/search"
)
func TestSearhOptionsBuilder(t *testing.T) {
type testCase struct {
name string
prepare func() *search.SearchOptions
expectedWhere string
expectedArgs []any
}
for _, tc := range []testCase{
{
name: "Empty fields",
prepare: func() *search.SearchOptions {
return search.NewSearchOptions().
Build()
},
expectedWhere: "",
expectedArgs: []any{},
},
{
name: "All fields",
prepare: func() *search.SearchOptions {
return search.NewSearchOptions().
WithName("foo").
WithDescription("bar").
WithRepository("buz").
WithProvides("test").
Build()
},
expectedWhere: "name LIKE ? AND description LIKE ? AND repository = ? AND json_array_contains(provides, ?)",
expectedArgs: []any{"%foo%", "%bar%", "buz", "test"},
},
} {
t.Run(tc.name, func(t *testing.T) {
whereClause, args := tc.prepare().WhereClause()
assert.Equal(t, tc.expectedWhere, whereClause)
assert.ElementsMatch(t, tc.expectedArgs, args)
})
}
}