forked from Plemya-x/ALR
		
	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:
		
							
								
								
									
										738
									
								
								internal/build/build.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										738
									
								
								internal/build/build.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,738 @@ | ||||
| // 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/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/types" | ||||
| ) | ||||
|  | ||||
| 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 | ||||
| } | ||||
							
								
								
									
										286
									
								
								internal/build/build_internal_test.need-to-update
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								internal/build/build_internal_test.need-to-update
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,286 @@ | ||||
| // 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/internal/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/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) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										69
									
								
								internal/build/cache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								internal/build/cache.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										74
									
								
								internal/build/checker.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								internal/build/checker.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,74 @@ | ||||
| // 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/manager" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/types" | ||||
| ) | ||||
|  | ||||
| type Checker struct { | ||||
| 	mgr manager.Manager | ||||
| } | ||||
|  | ||||
| func (c *Checker) PerformChecks( | ||||
| 	ctx context.Context, | ||||
| 	input *BuildInput, | ||||
| 	vars *types.BuildVars, | ||||
| ) (bool, error) { | ||||
| 	if !cpu.IsCompatibleWith(cpu.Arch(), vars.Architectures) { // Проверяем совместимость архитектуры | ||||
| 		cont, err := cliutils.YesNoPrompt( | ||||
| 			ctx, | ||||
| 			gotext.Get("Your system's CPU architecture doesn't match this package. Do you want to build anyway?"), | ||||
| 			input.opts.Interactive, | ||||
| 			true, | ||||
| 		) | ||||
| 		if err != nil { | ||||
| 			return false, err | ||||
| 		} | ||||
|  | ||||
| 		if !cont { | ||||
| 			return false, nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	installed, err := c.mgr.ListInstalled(nil) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	filename, err := pkgFileName(input, vars) | ||||
| 	if err != nil { | ||||
| 		return false, err | ||||
| 	} | ||||
|  | ||||
| 	if instVer, ok := installed[filename]; ok { // Если пакет уже установлен, выводим предупреждение | ||||
| 		slog.Warn(gotext.Get("This package is already installed"), | ||||
| 			"name", vars.Name, | ||||
| 			"version", instVer, | ||||
| 		) | ||||
| 	} | ||||
|  | ||||
| 	return true, nil | ||||
| } | ||||
							
								
								
									
										71
									
								
								internal/build/dirs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								internal/build/dirs.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,71 @@ | ||||
| // 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) | ||||
| } | ||||
							
								
								
									
										96
									
								
								internal/build/find_deps/alt_linux.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								internal/build/find_deps/alt_linux.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,96 @@ | ||||
| // 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) | ||||
| 	}) | ||||
| } | ||||
							
								
								
									
										39
									
								
								internal/build/find_deps/empty.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								internal/build/find_deps/empty.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										118
									
								
								internal/build/find_deps/fedora.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								internal/build/find_deps/fedora.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| // 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) | ||||
| 		}) | ||||
| } | ||||
							
								
								
									
										58
									
								
								internal/build/find_deps/find_deps.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								internal/build/find_deps/find_deps.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| // 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/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/types" | ||||
| ) | ||||
|  | ||||
| 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) | ||||
| } | ||||
							
								
								
									
										54
									
								
								internal/build/installer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								internal/build/installer.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,54 @@ | ||||
| // 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/internal/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 | ||||
| } | ||||
							
								
								
									
										52
									
								
								internal/build/main_build.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								internal/build/main_build.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,52 @@ | ||||
| // 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/internal/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 | ||||
| } | ||||
							
								
								
									
										40
									
								
								internal/build/safe_common.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								internal/build/safe_common.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | ||||
| // ALR - Any Linux Repository | ||||
| // Copyright (C) 2025 The ALR Authors | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| 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) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										150
									
								
								internal/build/safe_installer.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										150
									
								
								internal/build/safe_installer.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,150 @@ | ||||
| // ALR - Any Linux Repository | ||||
| // Copyright (C) 2025 The ALR Authors | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| package build | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"log/slog" | ||||
| 	"net/rpc" | ||||
| 	"os" | ||||
| 	"os/exec" | ||||
| 	"sync" | ||||
| 	"syscall" | ||||
|  | ||||
| 	"github.com/hashicorp/go-plugin" | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" | ||||
| ) | ||||
|  | ||||
| type InstallerPlugin struct { | ||||
| 	Impl InstallerExecutor | ||||
| } | ||||
|  | ||||
| type InstallerRPC struct { | ||||
| 	client *rpc.Client | ||||
| } | ||||
|  | ||||
| type InstallerRPCServer struct { | ||||
| 	Impl InstallerExecutor | ||||
| } | ||||
|  | ||||
| type InstallArgs struct { | ||||
| 	PackagesOrPaths []string | ||||
| 	Opts            *manager.Opts | ||||
| } | ||||
|  | ||||
| func (r *InstallerRPC) InstallLocal(paths []string, opts *manager.Opts) error { | ||||
| 	return r.client.Call("Plugin.InstallLocal", &InstallArgs{ | ||||
| 		PackagesOrPaths: paths, | ||||
| 		Opts:            opts, | ||||
| 	}, nil) | ||||
| } | ||||
|  | ||||
| func (s *InstallerRPCServer) InstallLocal(args *InstallArgs, reply *struct{}) error { | ||||
| 	return s.Impl.InstallLocal(args.PackagesOrPaths, args.Opts) | ||||
| } | ||||
|  | ||||
| func (r *InstallerRPC) Install(pkgs []string, opts *manager.Opts) error { | ||||
| 	return r.client.Call("Plugin.Install", &InstallArgs{ | ||||
| 		PackagesOrPaths: pkgs, | ||||
| 		Opts:            opts, | ||||
| 	}, nil) | ||||
| } | ||||
|  | ||||
| func (s *InstallerRPCServer) Install(args *InstallArgs, reply *struct{}) error { | ||||
| 	return s.Impl.Install(args.PackagesOrPaths, args.Opts) | ||||
| } | ||||
|  | ||||
| func (r *InstallerRPC) RemoveAlreadyInstalled(paths []string) ([]string, error) { | ||||
| 	var val []string | ||||
| 	err := r.client.Call("Plugin.RemoveAlreadyInstalled", paths, &val) | ||||
| 	return val, err | ||||
| } | ||||
|  | ||||
| func (s *InstallerRPCServer) RemoveAlreadyInstalled(pkgs []string, res *[]string) error { | ||||
| 	vars, err := s.Impl.RemoveAlreadyInstalled(pkgs) | ||||
| 	if err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| 	*res = vars | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| func (p *InstallerPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { | ||||
| 	return &InstallerRPC{client: c}, nil | ||||
| } | ||||
|  | ||||
| func (p *InstallerPlugin) Server(*plugin.MuxBroker) (interface{}, error) { | ||||
| 	return &InstallerRPCServer{Impl: p.Impl}, nil | ||||
| } | ||||
|  | ||||
| func GetSafeInstaller() (InstallerExecutor, func(), error) { | ||||
| 	var err error | ||||
|  | ||||
| 	executable, err := os.Executable() | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
| 	cmd := exec.Command(executable, "_internal-installer") | ||||
| 	setCommonCmdEnv(cmd) | ||||
|  | ||||
| 	slog.Debug("safe installer setup", "uid", syscall.Getuid(), "gid", syscall.Getgid()) | ||||
|  | ||||
| 	client := plugin.NewClient(&plugin.ClientConfig{ | ||||
| 		HandshakeConfig: HandshakeConfig, | ||||
| 		Plugins:         pluginMap, | ||||
| 		Cmd:             cmd, | ||||
| 		Logger:          logger.GetHCLoggerAdapter(), | ||||
| 		SkipHostEnv:     true, | ||||
| 		UnixSocketConfig: &plugin.UnixSocketConfig{ | ||||
| 			Group: "alr", | ||||
| 		}, | ||||
| 		SyncStderr: os.Stderr, | ||||
| 	}) | ||||
| 	rpcClient, err := client.Client() | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
|  | ||||
| 	var cleanupOnce sync.Once | ||||
| 	cleanup := func() { | ||||
| 		cleanupOnce.Do(func() { | ||||
| 			client.Kill() | ||||
| 		}) | ||||
| 	} | ||||
|  | ||||
| 	defer func() { | ||||
| 		if err != nil { | ||||
| 			slog.Debug("close installer") | ||||
| 			cleanup() | ||||
| 		} | ||||
| 	}() | ||||
|  | ||||
| 	raw, err := rpcClient.Dispense("installer") | ||||
| 	if err != nil { | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
|  | ||||
| 	executor, ok := raw.(InstallerExecutor) | ||||
| 	if !ok { | ||||
| 		err = fmt.Errorf("dispensed object is not a ScriptExecutor (got %T)", raw) | ||||
| 		return nil, nil, err | ||||
| 	} | ||||
|  | ||||
| 	return executor, cleanup, nil | ||||
| } | ||||
							
								
								
									
										273
									
								
								internal/build/safe_script_executor.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								internal/build/safe_script_executor.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,273 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										445
									
								
								internal/build/script_executor.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										445
									
								
								internal/build/script_executor.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,445 @@ | ||||
| // 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" | ||||
|  | ||||
| 	finddeps "gitea.plemya-x.ru/Plemya-x/ALR/internal/build/find_deps" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/distro" | ||||
| 	"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" | ||||
| ) | ||||
|  | ||||
| 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 | ||||
| } | ||||
							
								
								
									
										53
									
								
								internal/build/script_resolver.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								internal/build/script_resolver.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,53 @@ | ||||
| // ALR - Any Linux Repository | ||||
| // Copyright (C) 2025 The ALR Authors | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| package build | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"path/filepath" | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/db" | ||||
| ) | ||||
|  | ||||
| type ScriptResolver struct { | ||||
| 	cfg Config | ||||
| } | ||||
|  | ||||
| type ScriptInfo struct { | ||||
| 	Script     string | ||||
| 	Repository string | ||||
| } | ||||
|  | ||||
| func (s *ScriptResolver) ResolveScript( | ||||
| 	ctx context.Context, | ||||
| 	pkg *db.Package, | ||||
| ) *ScriptInfo { | ||||
| 	var repository, script string | ||||
|  | ||||
| 	repodir := s.cfg.GetPaths().RepoDir | ||||
| 	repository = pkg.Repository | ||||
| 	if pkg.BasePkgName != "" { | ||||
| 		script = filepath.Join(repodir, repository, pkg.BasePkgName, "alr.sh") | ||||
| 	} else { | ||||
| 		script = filepath.Join(repodir, repository, pkg.Name, "alr.sh") | ||||
| 	} | ||||
|  | ||||
| 	return &ScriptInfo{ | ||||
| 		Repository: repository, | ||||
| 		Script:     script, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										46
									
								
								internal/build/script_view.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								internal/build/script_view.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| // 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, | ||||
| 	) | ||||
| } | ||||
							
								
								
									
										86
									
								
								internal/build/source_downloader.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								internal/build/source_downloader.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										324
									
								
								internal/build/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								internal/build/utils.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,324 @@ | ||||
| // 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/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/types" | ||||
| ) | ||||
|  | ||||
| // Функция 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 | ||||
| } | ||||
							
								
								
									
										47
									
								
								internal/build/utils_internal_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								internal/build/utils_internal_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | ||||
| // ALR - Any Linux Repository | ||||
| // Copyright (C) 2025 The ALR Authors | ||||
| // | ||||
| // This program is free software: you can redistribute it and/or modify | ||||
| // it under the terms of the GNU General Public License as published by | ||||
| // the Free Software Foundation, either version 3 of the License, or | ||||
| // (at your option) any later version. | ||||
| // | ||||
| // This program is distributed in the hope that it will be useful, | ||||
| // but WITHOUT ANY WARRANTY; without even the implied warranty of | ||||
| // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | ||||
| // GNU General Public License for more details. | ||||
| // | ||||
| // You should have received a copy of the GNU General Public License | ||||
| // along with this program.  If not, see <http://www.gnu.org/licenses/>. | ||||
|  | ||||
| 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) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -26,9 +26,9 @@ import ( | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/config" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/db" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/manager" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/repos" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/repos" | ||||
| ) | ||||
|  | ||||
| type AppDeps struct { | ||||
|   | ||||
							
								
								
									
										123
									
								
								internal/distro/osrelease.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								internal/distro/osrelease.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										39
									
								
								internal/gen/funcs.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								internal/gen/funcs.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,39 @@ | ||||
| // 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] | ||||
| 	}, | ||||
| } | ||||
							
								
								
									
										118
									
								
								internal/gen/pip.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								internal/gen/pip.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,118 @@ | ||||
| // 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) | ||||
| } | ||||
							
								
								
									
										55
									
								
								internal/gen/tmpls/pip.tmpl.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								internal/gen/tmpls/pip.tmpl.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| # 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/* | ||||
| } | ||||
							
								
								
									
										169
									
								
								internal/manager/apk.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										169
									
								
								internal/manager/apk.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,169 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										155
									
								
								internal/manager/apt.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								internal/manager/apt.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,155 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										112
									
								
								internal/manager/apt_rpm.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								internal/manager/apt_rpm.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,112 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										35
									
								
								internal/manager/common.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								internal/manager/common.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										72
									
								
								internal/manager/common_rpm.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								internal/manager/common_rpm.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										120
									
								
								internal/manager/dnf.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								internal/manager/dnf.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										114
									
								
								internal/manager/managers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								internal/manager/managers.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										162
									
								
								internal/manager/pacman.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										162
									
								
								internal/manager/pacman.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,162 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										115
									
								
								internal/manager/yum.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								internal/manager/yum.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										115
									
								
								internal/manager/zypper.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								internal/manager/zypper.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| // 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 | ||||
| } | ||||
| @@ -30,7 +30,7 @@ import ( | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/cpu" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/db" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/distro" | ||||
| ) | ||||
|  | ||||
| type Opts struct { | ||||
|   | ||||
| @@ -27,8 +27,8 @@ import ( | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"golang.org/x/text/language" | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" | ||||
| ) | ||||
|  | ||||
| var info = &distro.OSRelease{ | ||||
|   | ||||
							
								
								
									
										38
									
								
								internal/parser/parser.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								internal/parser/parser.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										81
									
								
								internal/repos/find.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								internal/repos/find.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										147
									
								
								internal/repos/find_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										147
									
								
								internal/repos/find_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,147 @@ | ||||
| // 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/repos" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/types" | ||||
| ) | ||||
|  | ||||
| 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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										400
									
								
								internal/repos/pull.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										400
									
								
								internal/repos/pull.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,400 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										173
									
								
								internal/repos/pull_internal_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										173
									
								
								internal/repos/pull_internal_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,173 @@ | ||||
| // 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) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										145
									
								
								internal/repos/pull_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								internal/repos/pull_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,145 @@ | ||||
| // 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/repos" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/types" | ||||
| ) | ||||
|  | ||||
| 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) | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										43
									
								
								internal/repos/repos.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								internal/repos/repos.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,43 @@ | ||||
| // 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, | ||||
| 	} | ||||
| } | ||||
							
								
								
									
										266
									
								
								internal/repos/utils.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										266
									
								
								internal/repos/utils.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,266 @@ | ||||
| // 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/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/parser" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/types" | ||||
| ) | ||||
|  | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										66
									
								
								internal/search/search.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								internal/search/search.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										86
									
								
								internal/search/search_options_builder.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								internal/search/search_options_builder.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,86 @@ | ||||
| // 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 | ||||
| } | ||||
							
								
								
									
										65
									
								
								internal/search/search_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								internal/search/search_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,65 @@ | ||||
| // 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/internal/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) | ||||
| 		}) | ||||
| 	} | ||||
| } | ||||
| @@ -31,8 +31,8 @@ import ( | ||||
| 	"mvdan.cc/sh/v3/interp" | ||||
| 	"mvdan.cc/sh/v3/syntax" | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/overrides" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" | ||||
| ) | ||||
|  | ||||
| var ErrNotPointerToStruct = errors.New("val must be a pointer to a struct") | ||||
|   | ||||
| @@ -31,8 +31,8 @@ import ( | ||||
| 	"mvdan.cc/sh/v3/interp" | ||||
| 	"mvdan.cc/sh/v3/syntax" | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/distro" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder" | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" | ||||
| ) | ||||
|  | ||||
| type BuildVars struct { | ||||
|   | ||||
| @@ -27,9 +27,9 @@ import ( | ||||
| 	"mvdan.cc/sh/v3/interp" | ||||
| 	"mvdan.cc/sh/v3/syntax" | ||||
|  | ||||
| 	"gitea.plemya-x.ru/Plemya-x/ALR/internal/distro" | ||||
| 	"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/pkg/distro" | ||||
| ) | ||||
|  | ||||
| const testScript = ` | ||||
|   | ||||
| @@ -178,6 +178,68 @@ msgstr "" | ||||
| msgid "Error removing packages" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/build.go:417 | ||||
| msgid "Building package" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/build.go:446 | ||||
| msgid "The checksums array must be the same length as sources" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/build.go:488 | ||||
| msgid "Downloading sources" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/build.go:580 | ||||
| msgid "Installing dependencies" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/checker.go:43 | ||||
| msgid "" | ||||
| "Your system's CPU architecture doesn't match this package. Do you want to " | ||||
| "build anyway?" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/checker.go:67 | ||||
| msgid "This package is already installed" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/find_deps/alt_linux.go:35 | ||||
| msgid "Command not found on the system" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/find_deps/alt_linux.go:86 | ||||
| msgid "Provided dependency found" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/find_deps/alt_linux.go:93 | ||||
| msgid "Required dependency found" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/find_deps/empty.go:32 | ||||
| msgid "AutoProv is not implemented for this package format, so it's skipped" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/find_deps/empty.go:37 | ||||
| msgid "AutoReq is not implemented for this package format, so it's skipped" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/script_executor.go:236 | ||||
| msgid "Building package metadata" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/script_executor.go:366 | ||||
| msgid "Executing prepare()" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/script_executor.go:375 | ||||
| msgid "Executing build()" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/build/script_executor.go:404 internal/build/script_executor.go:424 | ||||
| msgid "Executing %s()" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/cliutils/app_builder/builder.go:75 | ||||
| msgid "Error loading config" | ||||
| msgstr "" | ||||
| @@ -321,6 +383,24 @@ msgstr "" | ||||
| msgid "ERROR" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/repos/pull.go:77 | ||||
| msgid "Pulling repository" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/repos/pull.go:113 | ||||
| msgid "Repository up to date" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/repos/pull.go:204 | ||||
| msgid "Git repository does not appear to be a valid ALR repo" | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/repos/pull.go:220 | ||||
| msgid "" | ||||
| "ALR repo's minimum ALR version is greater than the current version. Try " | ||||
| "updating ALR if something doesn't work." | ||||
| msgstr "" | ||||
|  | ||||
| #: internal/utils/cmd.go:97 | ||||
| msgid "Error on dropping capabilities" | ||||
| msgstr "" | ||||
| @@ -377,86 +457,6 @@ msgstr "" | ||||
| msgid "Error while running app" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/build.go:417 | ||||
| msgid "Building package" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/build.go:446 | ||||
| msgid "The checksums array must be the same length as sources" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/build.go:488 | ||||
| msgid "Downloading sources" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/build.go:580 | ||||
| msgid "Installing dependencies" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/checker.go:43 | ||||
| msgid "" | ||||
| "Your system's CPU architecture doesn't match this package. Do you want to " | ||||
| "build anyway?" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/checker.go:67 | ||||
| msgid "This package is already installed" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/find_deps/alt_linux.go:35 | ||||
| msgid "Command not found on the system" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/find_deps/alt_linux.go:86 | ||||
| msgid "Provided dependency found" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/find_deps/alt_linux.go:93 | ||||
| msgid "Required dependency found" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/find_deps/empty.go:32 | ||||
| msgid "AutoProv is not implemented for this package format, so it's skipped" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/find_deps/empty.go:37 | ||||
| msgid "AutoReq is not implemented for this package format, so it's skipped" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/script_executor.go:236 | ||||
| msgid "Building package metadata" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/script_executor.go:366 | ||||
| msgid "Executing prepare()" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/script_executor.go:375 | ||||
| msgid "Executing build()" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/build/script_executor.go:404 pkg/build/script_executor.go:424 | ||||
| msgid "Executing %s()" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/repos/pull.go:77 | ||||
| msgid "Pulling repository" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/repos/pull.go:113 | ||||
| msgid "Repository up to date" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/repos/pull.go:204 | ||||
| msgid "Git repository does not appear to be a valid ALR repo" | ||||
| msgstr "" | ||||
|  | ||||
| #: pkg/repos/pull.go:220 | ||||
| msgid "" | ||||
| "ALR repo's minimum ALR version is greater than the current version. Try " | ||||
| "updating ALR if something doesn't work." | ||||
| msgstr "" | ||||
|  | ||||
| #: refresh.go:30 | ||||
| msgid "Pull all repositories that have changed" | ||||
| msgstr "" | ||||
|   | ||||
| @@ -185,6 +185,72 @@ msgstr "Для команды remove ожидался хотя бы 1 аргум | ||||
| msgid "Error removing packages" | ||||
| msgstr "Ошибка при удалении пакетов" | ||||
|  | ||||
| #: internal/build/build.go:417 | ||||
| msgid "Building package" | ||||
| msgstr "Сборка пакета" | ||||
|  | ||||
| #: internal/build/build.go:446 | ||||
| msgid "The checksums array must be the same length as sources" | ||||
| msgstr "Массив контрольных сумм должен быть той же длины, что и источники" | ||||
|  | ||||
| #: internal/build/build.go:488 | ||||
| msgid "Downloading sources" | ||||
| msgstr "Скачивание источников" | ||||
|  | ||||
| #: internal/build/build.go:580 | ||||
| msgid "Installing dependencies" | ||||
| msgstr "Установка зависимостей" | ||||
|  | ||||
| #: internal/build/checker.go:43 | ||||
| msgid "" | ||||
| "Your system's CPU architecture doesn't match this package. Do you want to " | ||||
| "build anyway?" | ||||
| msgstr "" | ||||
| "Архитектура процессора вашей системы не соответствует этому пакету. Вы все " | ||||
| "равно хотите выполнить сборку?" | ||||
|  | ||||
| #: internal/build/checker.go:67 | ||||
| msgid "This package is already installed" | ||||
| msgstr "Этот пакет уже установлен" | ||||
|  | ||||
| #: internal/build/find_deps/alt_linux.go:35 | ||||
| msgid "Command not found on the system" | ||||
| msgstr "Команда не найдена в системе" | ||||
|  | ||||
| #: internal/build/find_deps/alt_linux.go:86 | ||||
| msgid "Provided dependency found" | ||||
| msgstr "Найденная предоставленная зависимость" | ||||
|  | ||||
| #: internal/build/find_deps/alt_linux.go:93 | ||||
| msgid "Required dependency found" | ||||
| msgstr "Найдена требуемая зависимость" | ||||
|  | ||||
| #: internal/build/find_deps/empty.go:32 | ||||
| msgid "AutoProv is not implemented for this package format, so it's skipped" | ||||
| msgstr "" | ||||
| "AutoProv не реализовано для этого формата пакета, поэтому будет пропущено" | ||||
|  | ||||
| #: internal/build/find_deps/empty.go:37 | ||||
| msgid "AutoReq is not implemented for this package format, so it's skipped" | ||||
| msgstr "" | ||||
| "AutoReq не реализовано для этого формата пакета, поэтому будет пропущено" | ||||
|  | ||||
| #: internal/build/script_executor.go:236 | ||||
| msgid "Building package metadata" | ||||
| msgstr "Сборка метаданных пакета" | ||||
|  | ||||
| #: internal/build/script_executor.go:366 | ||||
| msgid "Executing prepare()" | ||||
| msgstr "Выполнение prepare()" | ||||
|  | ||||
| #: internal/build/script_executor.go:375 | ||||
| msgid "Executing build()" | ||||
| msgstr "Выполнение build()" | ||||
|  | ||||
| #: internal/build/script_executor.go:404 internal/build/script_executor.go:424 | ||||
| msgid "Executing %s()" | ||||
| msgstr "Выполнение %s()" | ||||
|  | ||||
| #: internal/cliutils/app_builder/builder.go:75 | ||||
| msgid "Error loading config" | ||||
| msgstr "Ошибка при загрузке" | ||||
| @@ -331,6 +397,26 @@ msgstr "%s %s загружается — %s/с\n" | ||||
| msgid "ERROR" | ||||
| msgstr "ОШИБКА" | ||||
|  | ||||
| #: internal/repos/pull.go:77 | ||||
| msgid "Pulling repository" | ||||
| msgstr "Скачивание репозитория" | ||||
|  | ||||
| #: internal/repos/pull.go:113 | ||||
| msgid "Repository up to date" | ||||
| msgstr "Репозиторий уже обновлён" | ||||
|  | ||||
| #: internal/repos/pull.go:204 | ||||
| msgid "Git repository does not appear to be a valid ALR repo" | ||||
| msgstr "Репозиторий Git не поддерживается репозиторием ALR" | ||||
|  | ||||
| #: internal/repos/pull.go:220 | ||||
| msgid "" | ||||
| "ALR repo's minimum ALR version is greater than the current version. Try " | ||||
| "updating ALR if something doesn't work." | ||||
| msgstr "" | ||||
| "Минимальная версия ALR для ALR-репозитория выше текущей версии. Попробуйте " | ||||
| "обновить ALR, если что-то не работает." | ||||
|  | ||||
| #: internal/utils/cmd.go:97 | ||||
| msgid "Error on dropping capabilities" | ||||
| msgstr "Ошибка при понижении привилегий" | ||||
| @@ -387,92 +473,6 @@ msgstr "Показать справку" | ||||
| msgid "Error while running app" | ||||
| msgstr "Ошибка при запуске приложения" | ||||
|  | ||||
| #: pkg/build/build.go:417 | ||||
| msgid "Building package" | ||||
| msgstr "Сборка пакета" | ||||
|  | ||||
| #: pkg/build/build.go:446 | ||||
| msgid "The checksums array must be the same length as sources" | ||||
| msgstr "Массив контрольных сумм должен быть той же длины, что и источники" | ||||
|  | ||||
| #: pkg/build/build.go:488 | ||||
| msgid "Downloading sources" | ||||
| msgstr "Скачивание источников" | ||||
|  | ||||
| #: pkg/build/build.go:580 | ||||
| msgid "Installing dependencies" | ||||
| msgstr "Установка зависимостей" | ||||
|  | ||||
| #: pkg/build/checker.go:43 | ||||
| msgid "" | ||||
| "Your system's CPU architecture doesn't match this package. Do you want to " | ||||
| "build anyway?" | ||||
| msgstr "" | ||||
| "Архитектура процессора вашей системы не соответствует этому пакету. Вы все " | ||||
| "равно хотите выполнить сборку?" | ||||
|  | ||||
| #: pkg/build/checker.go:67 | ||||
| msgid "This package is already installed" | ||||
| msgstr "Этот пакет уже установлен" | ||||
|  | ||||
| #: pkg/build/find_deps/alt_linux.go:35 | ||||
| msgid "Command not found on the system" | ||||
| msgstr "Команда не найдена в системе" | ||||
|  | ||||
| #: pkg/build/find_deps/alt_linux.go:86 | ||||
| msgid "Provided dependency found" | ||||
| msgstr "Найденная предоставленная зависимость" | ||||
|  | ||||
| #: pkg/build/find_deps/alt_linux.go:93 | ||||
| msgid "Required dependency found" | ||||
| msgstr "Найдена требуемая зависимость" | ||||
|  | ||||
| #: pkg/build/find_deps/empty.go:32 | ||||
| msgid "AutoProv is not implemented for this package format, so it's skipped" | ||||
| msgstr "" | ||||
| "AutoProv не реализовано для этого формата пакета, поэтому будет пропущено" | ||||
|  | ||||
| #: pkg/build/find_deps/empty.go:37 | ||||
| msgid "AutoReq is not implemented for this package format, so it's skipped" | ||||
| msgstr "" | ||||
| "AutoReq не реализовано для этого формата пакета, поэтому будет пропущено" | ||||
|  | ||||
| #: pkg/build/script_executor.go:236 | ||||
| msgid "Building package metadata" | ||||
| msgstr "Сборка метаданных пакета" | ||||
|  | ||||
| #: pkg/build/script_executor.go:366 | ||||
| msgid "Executing prepare()" | ||||
| msgstr "Выполнение prepare()" | ||||
|  | ||||
| #: pkg/build/script_executor.go:375 | ||||
| msgid "Executing build()" | ||||
| msgstr "Выполнение build()" | ||||
|  | ||||
| #: pkg/build/script_executor.go:404 pkg/build/script_executor.go:424 | ||||
| msgid "Executing %s()" | ||||
| msgstr "Выполнение %s()" | ||||
|  | ||||
| #: pkg/repos/pull.go:77 | ||||
| msgid "Pulling repository" | ||||
| msgstr "Скачивание репозитория" | ||||
|  | ||||
| #: pkg/repos/pull.go:113 | ||||
| msgid "Repository up to date" | ||||
| msgstr "Репозиторий уже обновлён" | ||||
|  | ||||
| #: pkg/repos/pull.go:204 | ||||
| msgid "Git repository does not appear to be a valid ALR repo" | ||||
| msgstr "Репозиторий Git не поддерживается репозиторием ALR" | ||||
|  | ||||
| #: pkg/repos/pull.go:220 | ||||
| msgid "" | ||||
| "ALR repo's minimum ALR version is greater than the current version. Try " | ||||
| "updating ALR if something doesn't work." | ||||
| msgstr "" | ||||
| "Минимальная версия ALR для ALR-репозитория выше текущей версии. Попробуйте " | ||||
| "обновить ALR, если что-то не работает." | ||||
|  | ||||
| #: refresh.go:30 | ||||
| msgid "Pull all repositories that have changed" | ||||
| msgstr "Скачать все изменённые репозитории" | ||||
|   | ||||
		Reference in New Issue
	
	Block a user