diff --git a/assets/coverage-badge.svg b/assets/coverage-badge.svg index ffdf7ae..e1e8356 100644 --- a/assets/coverage-badge.svg +++ b/assets/coverage-badge.svg @@ -11,7 +11,7 @@ coverage coverage - 19.4% - 19.4% + 18.8% + 18.8% diff --git a/generators/plugin-generator/main.go b/generators/plugin-generator/main.go new file mode 100644 index 0000000..91b7a0c --- /dev/null +++ b/generators/plugin-generator/main.go @@ -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 . + +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 . + + +`) + + 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) + } +} diff --git a/internal.go b/internal.go index bbea3db..987529c 100644 --- a/internal.go +++ b/internal.go @@ -125,7 +125,7 @@ func InternalInstallCmd() *cli.Command { plugin.Serve(&plugin.ServeConfig{ HandshakeConfig: build.HandshakeConfig, Plugins: map[string]plugin.Plugin{ - "installer": &build.InstallerPlugin{ + "installer": &build.InstallerExecutorPlugin{ Impl: build.NewInstaller( manager.Detect(), ), diff --git a/internal/build/build.go b/internal/build/build.go index ae3b732..8b33905 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -176,25 +176,6 @@ type ScriptResolverExecutor interface { ResolveScript(ctx context.Context, pkg *alrsh.Package) *ScriptInfo } -type ScriptExecutor interface { - ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error) - ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error) - PrepareDirs( - ctx context.Context, - input *BuildInput, - basePkg string, - ) error - ExecuteSecondPass( - ctx context.Context, - input *BuildInput, - sf *alrsh.ScriptFile, - varsOfPackages []*alrsh.Package, - repoDeps []string, - builtDeps []*BuiltDep, - basePkg string, - ) ([]*BuiltDep, error) -} - type CacheExecutor interface { CheckForBuiltPackage(ctx context.Context, input *BuildInput, vars *alrsh.Package) (string, bool, error) } @@ -211,14 +192,6 @@ type CheckerExecutor interface { ) (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 { Sources []string Checksums []string @@ -499,6 +472,7 @@ func (b *Builder) removeBuildDeps(ctx context.Context, input interface { if remove { err = b.installerExecutor.Remove( + ctx, deps, &manager.Opts{ NoConfirm: !input.BuildOpts().Interactive, @@ -545,6 +519,7 @@ func (b *Builder) InstallALRPackages( } err = b.installerExecutor.InstallLocal( + ctx, GetBuiltPaths(res), &manager.Opts{ NoConfirm: !input.BuildOpts().Interactive, @@ -645,7 +620,7 @@ func (i *Builder) installBuildDeps( var deps []string var err error if len(pkgs) > 0 { - deps, err = i.installerExecutor.RemoveAlreadyInstalled(pkgs) + deps, err = i.installerExecutor.RemoveAlreadyInstalled(ctx, pkgs) if err != nil { return nil, nil, err } @@ -668,7 +643,7 @@ func (i *Builder) installOptDeps( pkgs []string, ) ([]*BuiltDep, error) { var builtDeps []*BuiltDep - optDeps, err := i.installerExecutor.RemoveAlreadyInstalled(pkgs) + optDeps, err := i.installerExecutor.RemoveAlreadyInstalled(ctx, pkgs) if err != nil { return nil, err } @@ -710,7 +685,7 @@ func (i *Builder) InstallPkgs( } if len(builtDeps) > 0 { - err = i.installerExecutor.InstallLocal(GetBuiltPaths(builtDeps), &manager.Opts{ + err = i.installerExecutor.InstallLocal(ctx, GetBuiltPaths(builtDeps), &manager.Opts{ NoConfirm: !input.BuildOpts().Interactive, }) if err != nil { @@ -719,7 +694,7 @@ func (i *Builder) InstallPkgs( } if len(repoDeps) > 0 { - err = i.installerExecutor.Install(repoDeps, &manager.Opts{ + err = i.installerExecutor.Install(ctx, repoDeps, &manager.Opts{ NoConfirm: !input.BuildOpts().Interactive, }) if err != nil { diff --git a/internal/build/installer.go b/internal/build/installer.go index eba512e..80973d5 100644 --- a/internal/build/installer.go +++ b/internal/build/installer.go @@ -17,6 +17,8 @@ package build import ( + "context" + "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 } -func (i *Installer) InstallLocal(paths []string, opts *manager.Opts) error { +func (i *Installer) InstallLocal(ctx context.Context, paths []string, opts *manager.Opts) error { return i.mgr.InstallLocal(opts, paths...) } -func (i *Installer) Install(pkgs []string, opts *manager.Opts) error { +func (i *Installer) Install(ctx context.Context, pkgs []string, opts *manager.Opts) error { return i.mgr.Install(opts, pkgs...) } -func (i *Installer) 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...) } -func (i *Installer) RemoveAlreadyInstalled(pkgs []string) ([]string, error) { +func (i *Installer) RemoveAlreadyInstalled(ctx context.Context, pkgs []string) ([]string, error) { filteredPackages := []string{} for _, dep := range pkgs { diff --git a/internal/build/plugins.go b/internal/build/plugins.go new file mode 100644 index 0000000..62a605a --- /dev/null +++ b/internal/build/plugins.go @@ -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 . + +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 +} diff --git a/internal/build/plugins_executors.go b/internal/build/plugins_executors.go new file mode 100644 index 0000000..cd2dfa5 --- /dev/null +++ b/internal/build/plugins_executors.go @@ -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 . + +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) +} diff --git a/internal/build/plugins_executors_gen.go b/internal/build/plugins_executors_gen.go new file mode 100644 index 0000000..9469262 --- /dev/null +++ b/internal/build/plugins_executors_gen.go @@ -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 . + +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 +} diff --git a/internal/build/safe_common.go b/internal/build/safe_common.go deleted file mode 100644 index 33cb66f..0000000 --- a/internal/build/safe_common.go +++ /dev/null @@ -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 . - -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) - } - } -} diff --git a/internal/build/safe_installer.go b/internal/build/safe_installer.go deleted file mode 100644 index e7c8447..0000000 --- a/internal/build/safe_installer.go +++ /dev/null @@ -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 . - -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 -} diff --git a/internal/build/safe_script_executor.go b/internal/build/safe_script_executor.go deleted file mode 100644 index 3342e7b..0000000 --- a/internal/build/safe_script_executor.go +++ /dev/null @@ -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 . - -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 -} diff --git a/internal/translations/default.pot b/internal/translations/default.pot index 354adf8..2d098e1 100644 --- a/internal/translations/default.pot +++ b/internal/translations/default.pot @@ -218,23 +218,23 @@ msgstr "" msgid "Error removing packages" msgstr "" -#: internal/build/build.go:378 +#: internal/build/build.go:351 msgid "Building package" msgstr "" -#: internal/build/build.go:407 +#: internal/build/build.go:380 msgid "The checksums array must be the same length as sources" msgstr "" -#: internal/build/build.go:449 +#: internal/build/build.go:422 msgid "Downloading sources" msgstr "" -#: internal/build/build.go:495 +#: internal/build/build.go:468 msgid "Would you like to remove the build dependencies?" msgstr "" -#: internal/build/build.go:571 +#: internal/build/build.go:546 msgid "Installing dependencies" msgstr "" diff --git a/internal/translations/po/ru/default.po b/internal/translations/po/ru/default.po index f2c9932..2c054f1 100644 --- a/internal/translations/po/ru/default.po +++ b/internal/translations/po/ru/default.po @@ -225,23 +225,23 @@ msgstr "Для команды remove ожидался хотя бы 1 аргум msgid "Error removing packages" msgstr "Ошибка при удалении пакетов" -#: internal/build/build.go:378 +#: internal/build/build.go:351 msgid "Building package" msgstr "Сборка пакета" -#: internal/build/build.go:407 +#: internal/build/build.go:380 msgid "The checksums array must be the same length as sources" msgstr "Массив контрольных сумм должен быть той же длины, что и источники" -#: internal/build/build.go:449 +#: internal/build/build.go:422 msgid "Downloading sources" msgstr "Скачивание источников" -#: internal/build/build.go:495 +#: internal/build/build.go:468 msgid "Would you like to remove the build dependencies?" msgstr "Хотели бы вы удалить зависимости сборки?" -#: internal/build/build.go:571 +#: internal/build/build.go:546 msgid "Installing dependencies" msgstr "Установка зависимостей"