refactor: generate plugin executors

This commit is contained in:
2025-07-07 13:56:09 +03:00
parent 4899e203bb
commit 1cc408ad7d
13 changed files with 913 additions and 522 deletions

View File

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

Before

Width:  |  Height:  |  Size: 926 B

After

Width:  |  Height:  |  Size: 926 B

View File

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

View File

@ -125,7 +125,7 @@ func InternalInstallCmd() *cli.Command {
plugin.Serve(&plugin.ServeConfig{ plugin.Serve(&plugin.ServeConfig{
HandshakeConfig: build.HandshakeConfig, HandshakeConfig: build.HandshakeConfig,
Plugins: map[string]plugin.Plugin{ Plugins: map[string]plugin.Plugin{
"installer": &build.InstallerPlugin{ "installer": &build.InstallerExecutorPlugin{
Impl: build.NewInstaller( Impl: build.NewInstaller(
manager.Detect(), manager.Detect(),
), ),

View File

@ -176,25 +176,6 @@ type ScriptResolverExecutor interface {
ResolveScript(ctx context.Context, pkg *alrsh.Package) *ScriptInfo ResolveScript(ctx context.Context, pkg *alrsh.Package) *ScriptInfo
} }
type ScriptExecutor interface {
ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error)
ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error)
PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error
ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *alrsh.ScriptFile,
varsOfPackages []*alrsh.Package,
repoDeps []string,
builtDeps []*BuiltDep,
basePkg string,
) ([]*BuiltDep, error)
}
type CacheExecutor interface { type CacheExecutor interface {
CheckForBuiltPackage(ctx context.Context, input *BuildInput, vars *alrsh.Package) (string, bool, error) CheckForBuiltPackage(ctx context.Context, input *BuildInput, vars *alrsh.Package) (string, bool, error)
} }
@ -211,14 +192,6 @@ type CheckerExecutor interface {
) (bool, error) ) (bool, error)
} }
type InstallerExecutor interface {
InstallLocal(paths []string, opts *manager.Opts) error
Install(pkgs []string, opts *manager.Opts) error
Remove(pkgs []string, opts *manager.Opts) error
RemoveAlreadyInstalled(pkgs []string) ([]string, error)
}
type SourcesInput struct { type SourcesInput struct {
Sources []string Sources []string
Checksums []string Checksums []string
@ -499,6 +472,7 @@ func (b *Builder) removeBuildDeps(ctx context.Context, input interface {
if remove { if remove {
err = b.installerExecutor.Remove( err = b.installerExecutor.Remove(
ctx,
deps, deps,
&manager.Opts{ &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive, NoConfirm: !input.BuildOpts().Interactive,
@ -545,6 +519,7 @@ func (b *Builder) InstallALRPackages(
} }
err = b.installerExecutor.InstallLocal( err = b.installerExecutor.InstallLocal(
ctx,
GetBuiltPaths(res), GetBuiltPaths(res),
&manager.Opts{ &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive, NoConfirm: !input.BuildOpts().Interactive,
@ -645,7 +620,7 @@ func (i *Builder) installBuildDeps(
var deps []string var deps []string
var err error var err error
if len(pkgs) > 0 { if len(pkgs) > 0 {
deps, err = i.installerExecutor.RemoveAlreadyInstalled(pkgs) deps, err = i.installerExecutor.RemoveAlreadyInstalled(ctx, pkgs)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@ -668,7 +643,7 @@ func (i *Builder) installOptDeps(
pkgs []string, pkgs []string,
) ([]*BuiltDep, error) { ) ([]*BuiltDep, error) {
var builtDeps []*BuiltDep var builtDeps []*BuiltDep
optDeps, err := i.installerExecutor.RemoveAlreadyInstalled(pkgs) optDeps, err := i.installerExecutor.RemoveAlreadyInstalled(ctx, pkgs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -710,7 +685,7 @@ func (i *Builder) InstallPkgs(
} }
if len(builtDeps) > 0 { if len(builtDeps) > 0 {
err = i.installerExecutor.InstallLocal(GetBuiltPaths(builtDeps), &manager.Opts{ err = i.installerExecutor.InstallLocal(ctx, GetBuiltPaths(builtDeps), &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive, NoConfirm: !input.BuildOpts().Interactive,
}) })
if err != nil { if err != nil {
@ -719,7 +694,7 @@ func (i *Builder) InstallPkgs(
} }
if len(repoDeps) > 0 { if len(repoDeps) > 0 {
err = i.installerExecutor.Install(repoDeps, &manager.Opts{ err = i.installerExecutor.Install(ctx, repoDeps, &manager.Opts{
NoConfirm: !input.BuildOpts().Interactive, NoConfirm: !input.BuildOpts().Interactive,
}) })
if err != nil { if err != nil {

View File

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

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

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

View File

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

View File

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

View File

@ -1,40 +0,0 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package build
import (
"os"
"os/exec"
"strings"
)
func setCommonCmdEnv(cmd *exec.Cmd) {
cmd.Env = []string{
"HOME=/var/cache/alr",
"LOGNAME=alr",
"USER=alr",
"PATH=/usr/bin:/bin:/usr/local/bin",
}
for _, env := range os.Environ() {
if strings.HasPrefix(env, "LANG=") ||
strings.HasPrefix(env, "LANGUAGE=") ||
strings.HasPrefix(env, "LC_") ||
strings.HasPrefix(env, "ALR_LOG_LEVEL=") {
cmd.Env = append(cmd.Env, env)
}
}
}

View File

@ -1,161 +0,0 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package build
import (
"fmt"
"log/slog"
"net/rpc"
"os"
"os/exec"
"sync"
"syscall"
"github.com/hashicorp/go-plugin"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/manager"
)
type InstallerPlugin struct {
Impl InstallerExecutor
}
type InstallerRPC struct {
client *rpc.Client
}
type InstallerRPCServer struct {
Impl InstallerExecutor
}
type InstallArgs struct {
PackagesOrPaths []string
Opts *manager.Opts
}
func (r *InstallerRPC) InstallLocal(paths []string, opts *manager.Opts) error {
return r.client.Call("Plugin.InstallLocal", &InstallArgs{
PackagesOrPaths: paths,
Opts: opts,
}, nil)
}
func (s *InstallerRPCServer) InstallLocal(args *InstallArgs, reply *struct{}) error {
return s.Impl.InstallLocal(args.PackagesOrPaths, args.Opts)
}
func (r *InstallerRPC) Install(pkgs []string, opts *manager.Opts) error {
return r.client.Call("Plugin.Install", &InstallArgs{
PackagesOrPaths: pkgs,
Opts: opts,
}, nil)
}
func (s *InstallerRPCServer) Install(args *InstallArgs, reply *struct{}) error {
return s.Impl.Install(args.PackagesOrPaths, args.Opts)
}
func (r *InstallerRPC) Remove(pkgs []string, opts *manager.Opts) error {
return r.client.Call("Plugin.Remove", &InstallArgs{
PackagesOrPaths: pkgs,
Opts: opts,
}, nil)
}
func (s *InstallerRPCServer) Remove(args *InstallArgs, reply *struct{}) error {
return s.Impl.Remove(args.PackagesOrPaths, args.Opts)
}
func (r *InstallerRPC) RemoveAlreadyInstalled(paths []string) ([]string, error) {
var val []string
err := r.client.Call("Plugin.RemoveAlreadyInstalled", paths, &val)
return val, err
}
func (s *InstallerRPCServer) RemoveAlreadyInstalled(pkgs []string, res *[]string) error {
vars, err := s.Impl.RemoveAlreadyInstalled(pkgs)
if err != nil {
return err
}
*res = vars
return nil
}
func (p *InstallerPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &InstallerRPC{client: c}, nil
}
func (p *InstallerPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &InstallerRPCServer{Impl: p.Impl}, nil
}
func GetSafeInstaller() (InstallerExecutor, func(), error) {
var err error
executable, err := os.Executable()
if err != nil {
return nil, nil, err
}
cmd := exec.Command(executable, "_internal-installer")
setCommonCmdEnv(cmd)
slog.Debug("safe installer setup", "uid", syscall.Getuid(), "gid", syscall.Getgid())
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: HandshakeConfig,
Plugins: pluginMap,
Cmd: cmd,
Logger: logger.GetHCLoggerAdapter(),
SkipHostEnv: true,
UnixSocketConfig: &plugin.UnixSocketConfig{
Group: "alr",
},
SyncStderr: os.Stderr,
})
rpcClient, err := client.Client()
if err != nil {
return nil, nil, err
}
var cleanupOnce sync.Once
cleanup := func() {
cleanupOnce.Do(func() {
client.Kill()
})
}
defer func() {
if err != nil {
slog.Debug("close installer")
cleanup()
}
}()
raw, err := rpcClient.Dispense("installer")
if err != nil {
return nil, nil, err
}
executor, ok := raw.(InstallerExecutor)
if !ok {
err = fmt.Errorf("dispensed object is not a ScriptExecutor (got %T)", raw)
return nil, nil, err
}
return executor, cleanup, nil
}

View File

@ -1,273 +0,0 @@
// ALR - Any Linux Repository
// Copyright (C) 2025 The ALR Authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package build
import (
"context"
"fmt"
"log/slog"
"net/rpc"
"os"
"os/exec"
"sync"
"github.com/hashicorp/go-plugin"
"gitea.plemya-x.ru/Plemya-x/ALR/internal/logger"
"gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh"
)
var HandshakeConfig = plugin.HandshakeConfig{
ProtocolVersion: 1,
MagicCookieKey: "ALR_PLUGIN",
MagicCookieValue: "-",
}
type ScriptExecutorPlugin struct {
Impl ScriptExecutor
}
type ScriptExecutorRPCServer struct {
Impl ScriptExecutor
}
// =============================
//
// ReadScript
//
func (s *ScriptExecutorRPC) ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error) {
var resp *alrsh.ScriptFile
err := s.client.Call("Plugin.ReadScript", scriptPath, &resp)
return resp, err
}
func (s *ScriptExecutorRPCServer) ReadScript(scriptPath string, resp *alrsh.ScriptFile) error {
file, err := s.Impl.ReadScript(context.Background(), scriptPath)
if err != nil {
return err
}
*resp = *file
return nil
}
// =============================
//
// ExecuteFirstPass
//
type ExecuteFirstPassArgs struct {
Input *BuildInput
Sf *alrsh.ScriptFile
}
type ExecuteFirstPassResp struct {
BasePkg string
VarsOfPackages []*alrsh.Package
}
func (s *ScriptExecutorRPC) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error) {
var resp *ExecuteFirstPassResp
err := s.client.Call("Plugin.ExecuteFirstPass", &ExecuteFirstPassArgs{
Input: input,
Sf: sf,
}, &resp)
if err != nil {
return "", nil, err
}
return resp.BasePkg, resp.VarsOfPackages, nil
}
func (s *ScriptExecutorRPCServer) ExecuteFirstPass(args *ExecuteFirstPassArgs, resp *ExecuteFirstPassResp) error {
basePkg, varsOfPackages, err := s.Impl.ExecuteFirstPass(context.Background(), args.Input, args.Sf)
if err != nil {
return err
}
*resp = ExecuteFirstPassResp{
BasePkg: basePkg,
VarsOfPackages: varsOfPackages,
}
return nil
}
// =============================
//
// PrepareDirs
//
type PrepareDirsArgs struct {
Input *BuildInput
BasePkg string
}
func (s *ScriptExecutorRPC) PrepareDirs(
ctx context.Context,
input *BuildInput,
basePkg string,
) error {
err := s.client.Call("Plugin.PrepareDirs", &PrepareDirsArgs{
Input: input,
BasePkg: basePkg,
}, nil)
if err != nil {
return err
}
return err
}
func (s *ScriptExecutorRPCServer) PrepareDirs(args *PrepareDirsArgs, reply *struct{}) error {
err := s.Impl.PrepareDirs(
context.Background(),
args.Input,
args.BasePkg,
)
if err != nil {
return err
}
return err
}
// =============================
//
// ExecuteSecondPass
//
type ExecuteSecondPassArgs struct {
Input *BuildInput
Sf *alrsh.ScriptFile
VarsOfPackages []*alrsh.Package
RepoDeps []string
BuiltDeps []*BuiltDep
BasePkg string
}
func (s *ScriptExecutorRPC) ExecuteSecondPass(
ctx context.Context,
input *BuildInput,
sf *alrsh.ScriptFile,
varsOfPackages []*alrsh.Package,
repoDeps []string,
builtDeps []*BuiltDep,
basePkg string,
) ([]*BuiltDep, error) {
var resp []*BuiltDep
err := s.client.Call("Plugin.ExecuteSecondPass", &ExecuteSecondPassArgs{
Input: input,
Sf: sf,
VarsOfPackages: varsOfPackages,
RepoDeps: repoDeps,
BuiltDeps: builtDeps,
BasePkg: basePkg,
}, &resp)
if err != nil {
return nil, err
}
return resp, nil
}
func (s *ScriptExecutorRPCServer) ExecuteSecondPass(args *ExecuteSecondPassArgs, resp *[]*BuiltDep) error {
res, err := s.Impl.ExecuteSecondPass(
context.Background(),
args.Input,
args.Sf,
args.VarsOfPackages,
args.RepoDeps,
args.BuiltDeps,
args.BasePkg,
)
if err != nil {
return err
}
*resp = res
return err
}
//
// ============================
//
func (p *ScriptExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) {
return &ScriptExecutorRPCServer{Impl: p.Impl}, nil
}
func (p *ScriptExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) {
return &ScriptExecutorRPC{client: c}, nil
}
type ScriptExecutorRPC struct {
client *rpc.Client
}
var pluginMap = map[string]plugin.Plugin{
"script-executor": &ScriptExecutorPlugin{},
"installer": &InstallerPlugin{},
}
func GetSafeScriptExecutor() (ScriptExecutor, func(), error) {
var err error
executable, err := os.Executable()
if err != nil {
return nil, nil, err
}
cmd := exec.Command(executable, "_internal-safe-script-executor")
setCommonCmdEnv(cmd)
client := plugin.NewClient(&plugin.ClientConfig{
HandshakeConfig: HandshakeConfig,
Plugins: pluginMap,
Cmd: cmd,
Logger: logger.GetHCLoggerAdapter(),
SkipHostEnv: true,
UnixSocketConfig: &plugin.UnixSocketConfig{
Group: "alr",
},
SyncStderr: os.Stderr,
})
rpcClient, err := client.Client()
if err != nil {
return nil, nil, err
}
var cleanupOnce sync.Once
cleanup := func() {
cleanupOnce.Do(func() {
client.Kill()
})
}
defer func() {
if err != nil {
slog.Debug("close script-executor")
cleanup()
}
}()
raw, err := rpcClient.Dispense("script-executor")
if err != nil {
return nil, nil, err
}
executor, ok := raw.(ScriptExecutor)
if !ok {
err = fmt.Errorf("dispensed object is not a ScriptExecutor (got %T)", raw)
return nil, nil, err
}
return executor, cleanup, nil
}

View File

@ -218,23 +218,23 @@ msgstr ""
msgid "Error removing packages" msgid "Error removing packages"
msgstr "" msgstr ""
#: internal/build/build.go:378 #: internal/build/build.go:351
msgid "Building package" msgid "Building package"
msgstr "" msgstr ""
#: internal/build/build.go:407 #: internal/build/build.go:380
msgid "The checksums array must be the same length as sources" msgid "The checksums array must be the same length as sources"
msgstr "" msgstr ""
#: internal/build/build.go:449 #: internal/build/build.go:422
msgid "Downloading sources" msgid "Downloading sources"
msgstr "" msgstr ""
#: internal/build/build.go:495 #: internal/build/build.go:468
msgid "Would you like to remove the build dependencies?" msgid "Would you like to remove the build dependencies?"
msgstr "" msgstr ""
#: internal/build/build.go:571 #: internal/build/build.go:546
msgid "Installing dependencies" msgid "Installing dependencies"
msgstr "" msgstr ""

View File

@ -225,23 +225,23 @@ msgstr "Для команды remove ожидался хотя бы 1 аргум
msgid "Error removing packages" msgid "Error removing packages"
msgstr "Ошибка при удалении пакетов" msgstr "Ошибка при удалении пакетов"
#: internal/build/build.go:378 #: internal/build/build.go:351
msgid "Building package" msgid "Building package"
msgstr "Сборка пакета" msgstr "Сборка пакета"
#: internal/build/build.go:407 #: internal/build/build.go:380
msgid "The checksums array must be the same length as sources" msgid "The checksums array must be the same length as sources"
msgstr "Массив контрольных сумм должен быть той же длины, что и источники" msgstr "Массив контрольных сумм должен быть той же длины, что и источники"
#: internal/build/build.go:449 #: internal/build/build.go:422
msgid "Downloading sources" msgid "Downloading sources"
msgstr "Скачивание источников" msgstr "Скачивание источников"
#: internal/build/build.go:495 #: internal/build/build.go:468
msgid "Would you like to remove the build dependencies?" msgid "Would you like to remove the build dependencies?"
msgstr "Хотели бы вы удалить зависимости сборки?" msgstr "Хотели бы вы удалить зависимости сборки?"
#: internal/build/build.go:571 #: internal/build/build.go:546
msgid "Installing dependencies" msgid "Installing dependencies"
msgstr "Установка зависимостей" msgstr "Установка зависимостей"