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/e2e-tests/__snapshots__/TestE2EIssue129RepoTomlImportTest_issue-129-repo-toml-import-test_ubuntu-24.04_1.snap.stdout b/e2e-tests/__snapshots__/TestE2EIssue129RepoTomlImportTest_issue-129-repo-toml-import-test_ubuntu-24.04_1.snap.stdout new file mode 100755 index 0000000..2a17b58 --- /dev/null +++ b/e2e-tests/__snapshots__/TestE2EIssue129RepoTomlImportTest_issue-129-repo-toml-import-test_ubuntu-24.04_1.snap.stdout @@ -0,0 +1,5 @@ +- name: alr-repo + url: https://gitea.plemya-x.ru/Plemya-x/repo-for-tests + ref: main + mirrors: + - https://github.com/example/example.git diff --git a/e2e-tests/issue_129_repo_toml_import_test.go b/e2e-tests/issue_129_repo_toml_import_test.go new file mode 100644 index 0000000..33e0d1d --- /dev/null +++ b/e2e-tests/issue_129_repo_toml_import_test.go @@ -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 . + +//go:build e2e + +package e2etests_test + +import ( + "testing" + + "go.alt-gnome.ru/capytest" +) + +func TestE2EIssue129RepoTomlImportTest(t *testing.T) { + runMatrixSuite( + t, + "issue-129-repo-toml-import-test", + COMMON_SYSTEMS, + func(t *testing.T, r capytest.Runner) { + defaultPrepare(t, r) + + r.Command("alr", "config", "get", "repos"). + ExpectStdoutMatchesSnapshot(). + Run(t) + }, + ) +} diff --git a/generators/plugin-generator/main.go b/generators/plugin-generator/main.go new file mode 100644 index 0000000..a488c0e --- /dev/null +++ b/generators/plugin-generator/main.go @@ -0,0 +1,416 @@ +// ALR - Any Linux Repository +// Copyright (C) 2025 The ALR Authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "log" + "os" + "strings" + "text/template" + "unicode" + + "golang.org/x/text/cases" + "golang.org/x/text/language" +) + +type MethodInfo struct { + Name string + Params []ParamInfo + Results []ResultInfo + EntityName string +} + +type ParamInfo struct { + Name string + Type string +} + +type ResultInfo struct { + Name string + Type string + Index int +} + +func extractImports(node *ast.File) []string { + var imports []string + for _, imp := range node.Imports { + if imp.Path.Value != "" { + imports = append(imports, imp.Path.Value) + } + } + return imports +} + +func output(path string, buf bytes.Buffer) { + formatted, err := format.Source(buf.Bytes()) + if err != nil { + log.Fatalf("formatting: %v", err) + } + + outPath := strings.TrimSuffix(path, ".go") + "_gen.go" + outFile, err := os.Create(outPath) + if err != nil { + log.Fatalf("create file: %v", err) + } + _, err = outFile.Write(formatted) + if err != nil { + log.Fatalf("writing output: %v", err) + } + outFile.Close() +} + +func main() { + path := os.Getenv("GOFILE") + if path == "" { + log.Fatal("GOFILE must be set") + } + + if len(os.Args) < 2 { + log.Fatal("At least one entity name must be provided") + } + + entityNames := os.Args[1:] + + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, path, nil, parser.AllErrors) + if err != nil { + log.Fatalf("parsing file: %v", err) + } + + packageName := node.Name.Name + + // Find all specified entities + entityData := make(map[string][]*ast.Field) + + for _, decl := range node.Decls { + genDecl, ok := decl.(*ast.GenDecl) + if !ok || genDecl.Tok != token.TYPE { + continue + } + for _, spec := range genDecl.Specs { + typeSpec := spec.(*ast.TypeSpec) + for _, entityName := range entityNames { + if typeSpec.Name.Name == entityName { + interfaceType, ok := typeSpec.Type.(*ast.InterfaceType) + if !ok { + log.Fatalf("entity %s is not an interface", entityName) + } + entityData[entityName] = interfaceType.Methods.List + } + } + } + } + + // Verify all entities were found + for _, entityName := range entityNames { + if _, found := entityData[entityName]; !found { + log.Fatalf("interface %s not found", entityName) + } + } + + var buf bytes.Buffer + + buf.WriteString(` +// DO NOT EDIT MANUALLY. This file is generated. + +// ALR - Any Linux Repository +// Copyright (C) 2025 The ALR Authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + + +`) + + buf.WriteString(fmt.Sprintf("package %s\n", packageName)) + + // Generate base structures for all entities + baseStructs(&buf, entityNames, extractImports(node)) + + // Generate method-specific code for each entity + for _, entityName := range entityNames { + methods := parseMethodsFromFields(entityName, entityData[entityName]) + argsGen(&buf, methods) + } + + output(path, buf) +} + +func parseMethodsFromFields(entityName string, fields []*ast.Field) []MethodInfo { + var methods []MethodInfo + + for _, field := range fields { + if len(field.Names) == 0 { + continue + } + + methodName := field.Names[0].Name + funcType, ok := field.Type.(*ast.FuncType) + if !ok { + continue + } + + method := MethodInfo{ + Name: methodName, + EntityName: entityName, + } + + // Parse parameters, excluding context.Context + if funcType.Params != nil { + for i, param := range funcType.Params.List { + paramType := typeToString(param.Type) + // Skip context.Context parameters + if paramType == "context.Context" { + continue + } + if len(param.Names) == 0 { + method.Params = append(method.Params, ParamInfo{ + Name: fmt.Sprintf("Arg%d", i), + Type: paramType, + }) + } else { + for _, name := range param.Names { + method.Params = append(method.Params, ParamInfo{ + Name: cases.Title(language.Und, cases.NoLower).String(name.Name), + Type: paramType, + }) + } + } + } + } + + // Parse results + if funcType.Results != nil { + resultIndex := 0 + for _, result := range funcType.Results.List { + resultType := typeToString(result.Type) + if resultType == "error" { + continue // Skip error in response struct + } + + if len(result.Names) == 0 { + method.Results = append(method.Results, ResultInfo{ + Name: fmt.Sprintf("Result%d", resultIndex), + Type: resultType, + Index: resultIndex, + }) + } else { + for _, name := range result.Names { + method.Results = append(method.Results, ResultInfo{ + Name: cases.Title(language.Und, cases.NoLower).String(name.Name), + Type: resultType, + Index: resultIndex, + }) + } + } + resultIndex++ + } + } + + methods = append(methods, method) + } + + return methods +} + +func argsGen(buf *bytes.Buffer, methods []MethodInfo) { + // Add template functions first + funcMap := template.FuncMap{ + "lowerFirst": func(s string) string { + if len(s) == 0 { + return s + } + return strings.ToLower(s[:1]) + s[1:] + }, + "zeroValue": func(typeName string) string { + typeName = strings.TrimSpace(typeName) + + switch typeName { + case "string": + return "\"\"" + case "int", "int8", "int16", "int32", "int64": + return "0" + case "uint", "uint8", "uint16", "uint32", "uint64": + return "0" + case "float32", "float64": + return "0.0" + case "bool": + return "false" + } + + if strings.HasPrefix(typeName, "*") { + return "nil" + } + if strings.HasPrefix(typeName, "[]") || + strings.HasPrefix(typeName, "map[") || + strings.HasPrefix(typeName, "chan ") { + return "nil" + } + + if typeName == "interface{}" { + return "nil" + } + + // If external type: pkg.Type + if strings.Contains(typeName, ".") { + return typeName + "{}" + } + + // If starts with uppercase — likely struct + if len(typeName) > 0 && unicode.IsUpper(rune(typeName[0])) { + return typeName + "{}" + } + + return "nil" + }, + } + + argsTemplate := template.Must(template.New("args").Funcs(funcMap).Parse(` +{{range .}} +type {{.EntityName}}{{.Name}}Args struct { +{{range .Params}} {{.Name}} {{.Type}} +{{end}}} + +type {{.EntityName}}{{.Name}}Resp struct { +{{range .Results}} {{.Name}} {{.Type}} +{{end}}} + +func (s *{{.EntityName}}RPC) {{.Name}}(ctx context.Context, {{range $i, $p := .Params}}{{if $i}}, {{end}}{{lowerFirst $p.Name}} {{$p.Type}}{{end}}) ({{range $i, $r := .Results}}{{if $i}}, {{end}}{{$r.Type}}{{end}}{{if .Results}}, {{end}}error) { + var resp *{{.EntityName}}{{.Name}}Resp + err := s.client.Call("Plugin.{{.Name}}", &{{.EntityName}}{{.Name}}Args{ +{{range .Params}} {{.Name}}: {{lowerFirst .Name}}, +{{end}} }, &resp) + if err != nil { + return {{range $i, $r := .Results}}{{if $i}}, {{end}}{{zeroValue $r.Type}}{{end}}{{if .Results}}, {{end}}err + } + return {{range $i, $r := .Results}}{{if $i}}, {{end}}resp.{{$r.Name}}{{end}}{{if .Results}}, {{end}}nil +} + +func (s *{{.EntityName}}RPCServer) {{.Name}}(args *{{.EntityName}}{{.Name}}Args, resp *{{.EntityName}}{{.Name}}Resp) error { + {{if .Results}}{{range $i, $r := .Results}}{{if $i}}, {{end}}{{lowerFirst $r.Name}}{{end}}, err := {{else}}err := {{end}}s.Impl.{{.Name}}(context.Background(),{{range $i, $p := .Params}}{{if $i}}, {{end}}args.{{$p.Name}}{{end}}) + if err != nil { + return err + } + {{if .Results}}*resp = {{.EntityName}}{{.Name}}Resp{ +{{range .Results}} {{.Name}}: {{lowerFirst .Name}}, +{{end}} } + {{else}}*resp = {{.EntityName}}{{.Name}}Resp{} + {{end}}return nil +} +{{end}} +`)) + + err := argsTemplate.Execute(buf, methods) + if err != nil { + log.Fatalf("execute args template: %v", err) + } +} + +func typeToString(expr ast.Expr) string { + switch t := expr.(type) { + case *ast.Ident: + return t.Name + case *ast.StarExpr: + return "*" + typeToString(t.X) + case *ast.ArrayType: + return "[]" + typeToString(t.Elt) + case *ast.SelectorExpr: + xStr := typeToString(t.X) + if xStr == "context" && t.Sel.Name == "Context" { + return "context.Context" + } + return xStr + "." + t.Sel.Name + case *ast.InterfaceType: + return "interface{}" + default: + return "interface{}" + } +} + +func baseStructs(buf *bytes.Buffer, entityNames, imports []string) { + // Ensure "context" is included in imports + updatedImports := imports + hasContext := false + for _, imp := range imports { + if strings.Contains(imp, `"context"`) { + hasContext = true + break + } + } + if !hasContext { + updatedImports = append(updatedImports, `"context"`) + } + + contentTemplate := template.Must(template.New("").Parse(` +import ( + "net/rpc" + + "github.com/hashicorp/go-plugin" +{{range .Imports}} {{.}} +{{end}} +) + +{{range .EntityNames}} +type {{ . }}Plugin struct { + Impl {{ . }} +} + +type {{ . }}RPCServer struct { + Impl {{ . }} +} + +type {{ . }}RPC struct { + client *rpc.Client +} + +func (p *{{ . }}Plugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { + return &{{ . }}RPC{client: c}, nil +} + +func (p *{{ . }}Plugin) Server(*plugin.MuxBroker) (interface{}, error) { + return &{{ . }}RPCServer{Impl: p.Impl}, nil +} + +{{end}} +`)) + err := contentTemplate.Execute(buf, struct { + EntityNames []string + Imports []string + }{ + EntityNames: entityNames, + Imports: updatedImports, + }) + if err != nil { + log.Fatalf("execute template: %v", err) + } +} diff --git a/go.mod b/go.mod index fa734da..b0f26ad 100644 --- a/go.mod +++ b/go.mod @@ -34,8 +34,8 @@ require ( github.com/stretchr/testify v1.10.0 github.com/urfave/cli/v2 v2.25.7 github.com/vmihailenco/msgpack/v5 v5.3.5 - go.alt-gnome.ru/capytest v0.0.2 - go.alt-gnome.ru/capytest/providers/podman v0.0.2 + go.alt-gnome.ru/capytest v0.0.3-0.20250706082755-f20413e052f9 + go.alt-gnome.ru/capytest/providers/podman v0.0.3-0.20250706082755-f20413e052f9 go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4 golang.org/x/crypto v0.36.0 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 @@ -77,6 +77,9 @@ require ( github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/fatih/color v1.7.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/gkampitakis/ciinfo v0.3.2 // indirect + github.com/gkampitakis/go-diff v1.3.2 // indirect + github.com/gkampitakis/go-snaps v0.5.13 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-viper/mapstructure/v2 v2.3.0 // indirect @@ -101,7 +104,10 @@ require ( github.com/klauspost/compress v1.17.11 // indirect github.com/klauspost/pgzip v1.2.6 // indirect github.com/knadh/koanf/maps v0.1.2 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/maruel/natural v1.1.1 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect @@ -120,6 +126,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect github.com/shopspring/decimal v1.3.1 // indirect @@ -127,6 +134,10 @@ require ( github.com/spf13/cast v1.7.1 // indirect github.com/syndtr/goleveldb v1.0.0 // indirect github.com/therootcompany/xz v1.0.1 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/ulikunitz/xz v0.5.12 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect diff --git a/go.sum b/go.sum index 64d87f9..d77b9ea 100644 --- a/go.sum +++ b/go.sum @@ -104,6 +104,7 @@ github.com/connesc/cipherio v0.2.1 h1:FGtpTPMbKNNWByNrr9aEBtaJtXjqOzkIXNYJp6OEyc github.com/connesc/cipherio v0.2.1/go.mod h1:ukY0MWJDFnJEbXMQtOcn2VmTpRfzcTz4OoVrWGGJZcA= github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -134,6 +135,12 @@ github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7z github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= +github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.13 h1:Hhjmvv1WboSCxkR9iU2mj5PQ8tsz/y8ECGrIbjjPF8Q= +github.com/gkampitakis/go-snaps v0.5.13/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -286,6 +293,8 @@ github.com/leonelquinteros/gotext v1.7.0 h1:jcJmF4AXqyamP7vuw2MMIKs+O3jAEmvrc5JQ github.com/leonelquinteros/gotext v1.7.0/go.mod h1:qJdoQuERPpccw7L70uoU+K/BvTfRBHYsisCQyFLXyvw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= @@ -345,6 +354,7 @@ github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0 github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -358,6 +368,7 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -393,6 +404,16 @@ github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFd github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.12 h1:37Nm15o69RwBkXM0J6A5OlE67RZTfzUxTj8fB3dfcsc= github.com/ulikunitz/xz v0.5.12/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= @@ -413,8 +434,10 @@ gitlab.com/digitalxero/go-conventional-commit v1.0.7 h1:8/dO6WWG+98PMhlZowt/Yjui gitlab.com/digitalxero/go-conventional-commit v1.0.7/go.mod h1:05Xc2BFsSyC5tKhK0y+P3bs0AwUtNuTp+mTpbCU/DZ0= go.alt-gnome.ru/capytest v0.0.2 h1:clmvIqmYS86hhA1rsvivSSPpfOFkJTpbn38EQP7I3E8= go.alt-gnome.ru/capytest v0.0.2/go.mod h1:lvxPx3H6h+LPnStBFblgoT2wkjv0wbug3S14troykEg= -go.alt-gnome.ru/capytest/providers/podman v0.0.2 h1:fTQ9fmYiONgL8dJvyMB+irCfuojIVaomnqto6bl6HjU= -go.alt-gnome.ru/capytest/providers/podman v0.0.2/go.mod h1:Wpq1Ny3eMzADJpMJArA2TZGZbsviUBmawtEPcxnoerg= +go.alt-gnome.ru/capytest v0.0.3-0.20250706082755-f20413e052f9 h1:NST+V5LV/eLgs0p6PsuvfHiZ4UrIWqftCdifO8zgg0g= +go.alt-gnome.ru/capytest v0.0.3-0.20250706082755-f20413e052f9/go.mod h1:qiM8LARP+JBZr5mrDoVylOoqjrN0MAzvZ21NR9qMc0Y= +go.alt-gnome.ru/capytest/providers/podman v0.0.3-0.20250706082755-f20413e052f9 h1:VZclgdJxARvhZ6PIWWW2hQ6Ge4XeE36pzUr/U/y62bE= +go.alt-gnome.ru/capytest/providers/podman v0.0.3-0.20250706082755-f20413e052f9/go.mod h1:Wpq1Ny3eMzADJpMJArA2TZGZbsviUBmawtEPcxnoerg= go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4 h1:Ep54XceQlKhcCHl9awG+wWP4kz4kIP3c3Lzw/Gc/zwY= go.elara.ws/vercmp v0.0.0-20230622214216-0b2b067575c4/go.mod h1:/7PNW7nFnDR5W7UXZVc04gdVLR/wBNgkm33KgIz0OBk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/internal.go b/internal.go index bbea3db..3080540 100644 --- a/internal.go +++ b/internal.go @@ -84,6 +84,43 @@ func InternalBuildCmd() *cli.Command { } } +func InternalReposCmd() *cli.Command { + return &cli.Command{ + Name: "_internal-repos", + HideHelp: true, + Hidden: true, + Action: utils.RootNeededAction(func(ctx *cli.Context) error { + logger.SetupForGoPlugin() + + if err := utils.ExitIfCantDropCapsToAlrUser(); err != nil { + return err + } + + deps, err := appbuilder. + New(ctx.Context). + WithConfig(). + WithDB(). + WithReposNoPull(). + Build() + if err != nil { + return err + } + defer deps.Defer() + + pluginCfg := build.GetPluginServeCommonConfig() + pluginCfg.Plugins = map[string]plugin.Plugin{ + "repos": &build.ReposExecutorPlugin{ + Impl: build.NewRepos( + deps.Repos, + ), + }, + } + plugin.Serve(pluginCfg) + return nil + }), + } +} + func InternalInstallCmd() *cli.Command { return &cli.Command{ Name: "_internal-installer", @@ -125,7 +162,7 @@ func InternalInstallCmd() *cli.Command { plugin.Serve(&plugin.ServeConfig{ HandshakeConfig: build.HandshakeConfig, Plugins: map[string]plugin.Plugin{ - "installer": &build.InstallerPlugin{ + "installer": &build.InstallerExecutorPlugin{ Impl: build.NewInstaller( manager.Detect(), ), 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..75e6f3a --- /dev/null +++ b/internal/build/plugins.go @@ -0,0 +1,144 @@ +// ALR - Any Linux Repository +// Copyright (C) 2025 The ALR Authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package build + +import ( + "fmt" + "log/slog" + "os" + "os/exec" + "strings" + "sync" + + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-plugin" + + "gitea.plemya-x.ru/Plemya-x/ALR/internal/logger" +) + +var pluginMap = map[string]plugin.Plugin{ + "script-executor": &ScriptExecutorPlugin{}, + "installer": &InstallerExecutorPlugin{}, + "repos": &ReposExecutorPlugin{}, +} + +var HandshakeConfig = plugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "ALR_PLUGIN", + MagicCookieValue: "-", +} + +func setCommonCmdEnv(cmd *exec.Cmd) { + cmd.Env = []string{ + "HOME=/var/cache/alr", + "LOGNAME=alr", + "USER=alr", + "PATH=/usr/bin:/bin:/usr/local/bin", + } + for _, env := range os.Environ() { + if strings.HasPrefix(env, "LANG=") || + strings.HasPrefix(env, "LANGUAGE=") || + strings.HasPrefix(env, "LC_") || + strings.HasPrefix(env, "ALR_LOG_LEVEL=") { + cmd.Env = append(cmd.Env, env) + } + } +} + +func GetPluginServeCommonConfig() *plugin.ServeConfig { + return &plugin.ServeConfig{ + HandshakeConfig: HandshakeConfig, + Logger: hclog.New(&hclog.LoggerOptions{ + Name: "plugin", + Output: os.Stderr, + Level: hclog.Trace, + JSONFormat: true, + DisableTime: true, + }), + } +} + +func GetSafeInstaller() (InstallerExecutor, func(), error) { + return getSafeExecutor[InstallerExecutor]("_internal-installer", "installer") +} + +func GetSafeScriptExecutor() (ScriptExecutor, func(), error) { + return getSafeExecutor[ScriptExecutor]("_internal-safe-script-executor", "script-executor") +} + +func GetSafeReposExecutor() (ReposExecutor, func(), error) { + return getSafeExecutor[ReposExecutor]("_internal-repos", "repos") +} + +func getSafeExecutor[T any](subCommand, pluginName string) (T, func(), error) { + var err error + + executable, err := os.Executable() + if err != nil { + var zero T + return zero, nil, err + } + + cmd := exec.Command(executable, subCommand) + setCommonCmdEnv(cmd) + + client := plugin.NewClient(&plugin.ClientConfig{ + HandshakeConfig: HandshakeConfig, + Plugins: pluginMap, + Cmd: cmd, + Logger: logger.GetHCLoggerAdapter(), + SkipHostEnv: true, + UnixSocketConfig: &plugin.UnixSocketConfig{ + Group: "alr", + }, + SyncStderr: os.Stderr, + }) + rpcClient, err := client.Client() + if err != nil { + var zero T + return zero, nil, err + } + + var cleanupOnce sync.Once + cleanup := func() { + cleanupOnce.Do(func() { + client.Kill() + }) + } + + defer func() { + if err != nil { + slog.Debug("close executor") + cleanup() + } + }() + + raw, err := rpcClient.Dispense(pluginName) + if err != nil { + var zero T + return zero, nil, err + } + + executor, ok := raw.(T) + if !ok { + var zero T + err = fmt.Errorf("dispensed object is not a %T (got %T)", zero, raw) + return zero, nil, err + } + + return executor, cleanup, nil +} diff --git a/internal/build/plugins_executors.go b/internal/build/plugins_executors.go new file mode 100644 index 0000000..98d388e --- /dev/null +++ b/internal/build/plugins_executors.go @@ -0,0 +1,60 @@ +// ALR - Any Linux Repository +// Copyright (C) 2025 The ALR Authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package build + +import ( + "context" + + "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" +) + +//go:generate go run ../../generators/plugin-generator InstallerExecutor ScriptExecutor ReposExecutor + +// The Executors interfaces must use context.Context as the first parameter, +// because the plugin-generator cannot generate code without it. + +type InstallerExecutor interface { + InstallLocal(ctx context.Context, paths []string, opts *manager.Opts) error + Install(ctx context.Context, pkgs []string, opts *manager.Opts) error + Remove(ctx context.Context, pkgs []string, opts *manager.Opts) error + RemoveAlreadyInstalled(ctx context.Context, pkgs []string) ([]string, error) +} + +type ScriptExecutor interface { + ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error) + ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error) + PrepareDirs( + ctx context.Context, + input *BuildInput, + basePkg string, + ) error + ExecuteSecondPass( + ctx context.Context, + input *BuildInput, + sf *alrsh.ScriptFile, + varsOfPackages []*alrsh.Package, + repoDeps []string, + builtDeps []*BuiltDep, + basePkg string, + ) ([]*BuiltDep, error) +} + +type ReposExecutor interface { + PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) (types.Repo, error) +} diff --git a/internal/build/plugins_executors_gen.go b/internal/build/plugins_executors_gen.go new file mode 100644 index 0000000..b7e3417 --- /dev/null +++ b/internal/build/plugins_executors_gen.go @@ -0,0 +1,369 @@ +// DO NOT EDIT MANUALLY. This file is generated. + +// ALR - Any Linux Repository +// Copyright (C) 2025 The ALR Authors +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +package build + +import ( + "net/rpc" + + "context" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/manager" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" + "github.com/hashicorp/go-plugin" +) + +type InstallerExecutorPlugin struct { + Impl InstallerExecutor +} + +type InstallerExecutorRPCServer struct { + Impl InstallerExecutor +} + +type InstallerExecutorRPC struct { + client *rpc.Client +} + +func (p *InstallerExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { + return &InstallerExecutorRPC{client: c}, nil +} + +func (p *InstallerExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) { + return &InstallerExecutorRPCServer{Impl: p.Impl}, nil +} + +type ScriptExecutorPlugin struct { + Impl ScriptExecutor +} + +type ScriptExecutorRPCServer struct { + Impl ScriptExecutor +} + +type ScriptExecutorRPC struct { + client *rpc.Client +} + +func (p *ScriptExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { + return &ScriptExecutorRPC{client: c}, nil +} + +func (p *ScriptExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) { + return &ScriptExecutorRPCServer{Impl: p.Impl}, nil +} + +type ReposExecutorPlugin struct { + Impl ReposExecutor +} + +type ReposExecutorRPCServer struct { + Impl ReposExecutor +} + +type ReposExecutorRPC struct { + client *rpc.Client +} + +func (p *ReposExecutorPlugin) Client(b *plugin.MuxBroker, c *rpc.Client) (interface{}, error) { + return &ReposExecutorRPC{client: c}, nil +} + +func (p *ReposExecutorPlugin) Server(*plugin.MuxBroker) (interface{}, error) { + return &ReposExecutorRPCServer{Impl: p.Impl}, nil +} + +type InstallerExecutorInstallLocalArgs struct { + Paths []string + Opts *manager.Opts +} + +type InstallerExecutorInstallLocalResp struct { +} + +func (s *InstallerExecutorRPC) InstallLocal(ctx context.Context, paths []string, opts *manager.Opts) error { + var resp *InstallerExecutorInstallLocalResp + err := s.client.Call("Plugin.InstallLocal", &InstallerExecutorInstallLocalArgs{ + Paths: paths, + Opts: opts, + }, &resp) + if err != nil { + return err + } + return nil +} + +func (s *InstallerExecutorRPCServer) InstallLocal(args *InstallerExecutorInstallLocalArgs, resp *InstallerExecutorInstallLocalResp) error { + err := s.Impl.InstallLocal(context.Background(), args.Paths, args.Opts) + if err != nil { + return err + } + *resp = InstallerExecutorInstallLocalResp{} + return nil +} + +type InstallerExecutorInstallArgs struct { + Pkgs []string + Opts *manager.Opts +} + +type InstallerExecutorInstallResp struct { +} + +func (s *InstallerExecutorRPC) Install(ctx context.Context, pkgs []string, opts *manager.Opts) error { + var resp *InstallerExecutorInstallResp + err := s.client.Call("Plugin.Install", &InstallerExecutorInstallArgs{ + Pkgs: pkgs, + Opts: opts, + }, &resp) + if err != nil { + return err + } + return nil +} + +func (s *InstallerExecutorRPCServer) Install(args *InstallerExecutorInstallArgs, resp *InstallerExecutorInstallResp) error { + err := s.Impl.Install(context.Background(), args.Pkgs, args.Opts) + if err != nil { + return err + } + *resp = InstallerExecutorInstallResp{} + return nil +} + +type InstallerExecutorRemoveArgs struct { + Pkgs []string + Opts *manager.Opts +} + +type InstallerExecutorRemoveResp struct { +} + +func (s *InstallerExecutorRPC) Remove(ctx context.Context, pkgs []string, opts *manager.Opts) error { + var resp *InstallerExecutorRemoveResp + err := s.client.Call("Plugin.Remove", &InstallerExecutorRemoveArgs{ + Pkgs: pkgs, + Opts: opts, + }, &resp) + if err != nil { + return err + } + return nil +} + +func (s *InstallerExecutorRPCServer) Remove(args *InstallerExecutorRemoveArgs, resp *InstallerExecutorRemoveResp) error { + err := s.Impl.Remove(context.Background(), args.Pkgs, args.Opts) + if err != nil { + return err + } + *resp = InstallerExecutorRemoveResp{} + return nil +} + +type InstallerExecutorRemoveAlreadyInstalledArgs struct { + Pkgs []string +} + +type InstallerExecutorRemoveAlreadyInstalledResp struct { + Result0 []string +} + +func (s *InstallerExecutorRPC) RemoveAlreadyInstalled(ctx context.Context, pkgs []string) ([]string, error) { + var resp *InstallerExecutorRemoveAlreadyInstalledResp + err := s.client.Call("Plugin.RemoveAlreadyInstalled", &InstallerExecutorRemoveAlreadyInstalledArgs{ + Pkgs: pkgs, + }, &resp) + if err != nil { + return nil, err + } + return resp.Result0, nil +} + +func (s *InstallerExecutorRPCServer) RemoveAlreadyInstalled(args *InstallerExecutorRemoveAlreadyInstalledArgs, resp *InstallerExecutorRemoveAlreadyInstalledResp) error { + result0, err := s.Impl.RemoveAlreadyInstalled(context.Background(), args.Pkgs) + if err != nil { + return err + } + *resp = InstallerExecutorRemoveAlreadyInstalledResp{ + Result0: result0, + } + return nil +} + +type ScriptExecutorReadScriptArgs struct { + ScriptPath string +} + +type ScriptExecutorReadScriptResp struct { + Result0 *alrsh.ScriptFile +} + +func (s *ScriptExecutorRPC) ReadScript(ctx context.Context, scriptPath string) (*alrsh.ScriptFile, error) { + var resp *ScriptExecutorReadScriptResp + err := s.client.Call("Plugin.ReadScript", &ScriptExecutorReadScriptArgs{ + ScriptPath: scriptPath, + }, &resp) + if err != nil { + return nil, err + } + return resp.Result0, nil +} + +func (s *ScriptExecutorRPCServer) ReadScript(args *ScriptExecutorReadScriptArgs, resp *ScriptExecutorReadScriptResp) error { + result0, err := s.Impl.ReadScript(context.Background(), args.ScriptPath) + if err != nil { + return err + } + *resp = ScriptExecutorReadScriptResp{ + Result0: result0, + } + return nil +} + +type ScriptExecutorExecuteFirstPassArgs struct { + Input *BuildInput + Sf *alrsh.ScriptFile +} + +type ScriptExecutorExecuteFirstPassResp struct { + Result0 string + Result1 []*alrsh.Package +} + +func (s *ScriptExecutorRPC) ExecuteFirstPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile) (string, []*alrsh.Package, error) { + var resp *ScriptExecutorExecuteFirstPassResp + err := s.client.Call("Plugin.ExecuteFirstPass", &ScriptExecutorExecuteFirstPassArgs{ + Input: input, + Sf: sf, + }, &resp) + if err != nil { + return "", nil, err + } + return resp.Result0, resp.Result1, nil +} + +func (s *ScriptExecutorRPCServer) ExecuteFirstPass(args *ScriptExecutorExecuteFirstPassArgs, resp *ScriptExecutorExecuteFirstPassResp) error { + result0, result1, err := s.Impl.ExecuteFirstPass(context.Background(), args.Input, args.Sf) + if err != nil { + return err + } + *resp = ScriptExecutorExecuteFirstPassResp{ + Result0: result0, + Result1: result1, + } + return nil +} + +type ScriptExecutorPrepareDirsArgs struct { + Input *BuildInput + BasePkg string +} + +type ScriptExecutorPrepareDirsResp struct { +} + +func (s *ScriptExecutorRPC) PrepareDirs(ctx context.Context, input *BuildInput, basePkg string) error { + var resp *ScriptExecutorPrepareDirsResp + err := s.client.Call("Plugin.PrepareDirs", &ScriptExecutorPrepareDirsArgs{ + Input: input, + BasePkg: basePkg, + }, &resp) + if err != nil { + return err + } + return nil +} + +func (s *ScriptExecutorRPCServer) PrepareDirs(args *ScriptExecutorPrepareDirsArgs, resp *ScriptExecutorPrepareDirsResp) error { + err := s.Impl.PrepareDirs(context.Background(), args.Input, args.BasePkg) + if err != nil { + return err + } + *resp = ScriptExecutorPrepareDirsResp{} + return nil +} + +type ScriptExecutorExecuteSecondPassArgs struct { + Input *BuildInput + Sf *alrsh.ScriptFile + VarsOfPackages []*alrsh.Package + RepoDeps []string + BuiltDeps []*BuiltDep + BasePkg string +} + +type ScriptExecutorExecuteSecondPassResp struct { + Result0 []*BuiltDep +} + +func (s *ScriptExecutorRPC) ExecuteSecondPass(ctx context.Context, input *BuildInput, sf *alrsh.ScriptFile, varsOfPackages []*alrsh.Package, repoDeps []string, builtDeps []*BuiltDep, basePkg string) ([]*BuiltDep, error) { + var resp *ScriptExecutorExecuteSecondPassResp + err := s.client.Call("Plugin.ExecuteSecondPass", &ScriptExecutorExecuteSecondPassArgs{ + Input: input, + Sf: sf, + VarsOfPackages: varsOfPackages, + RepoDeps: repoDeps, + BuiltDeps: builtDeps, + BasePkg: basePkg, + }, &resp) + if err != nil { + return nil, err + } + return resp.Result0, nil +} + +func (s *ScriptExecutorRPCServer) ExecuteSecondPass(args *ScriptExecutorExecuteSecondPassArgs, resp *ScriptExecutorExecuteSecondPassResp) error { + result0, err := s.Impl.ExecuteSecondPass(context.Background(), args.Input, args.Sf, args.VarsOfPackages, args.RepoDeps, args.BuiltDeps, args.BasePkg) + if err != nil { + return err + } + *resp = ScriptExecutorExecuteSecondPassResp{ + Result0: result0, + } + return nil +} + +type ReposExecutorPullOneAndUpdateFromConfigArgs struct { + Repo *types.Repo +} + +type ReposExecutorPullOneAndUpdateFromConfigResp struct { + Result0 types.Repo +} + +func (s *ReposExecutorRPC) PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) (types.Repo, error) { + var resp *ReposExecutorPullOneAndUpdateFromConfigResp + err := s.client.Call("Plugin.PullOneAndUpdateFromConfig", &ReposExecutorPullOneAndUpdateFromConfigArgs{ + Repo: repo, + }, &resp) + if err != nil { + return types.Repo{}, err + } + return resp.Result0, nil +} + +func (s *ReposExecutorRPCServer) PullOneAndUpdateFromConfig(args *ReposExecutorPullOneAndUpdateFromConfigArgs, resp *ReposExecutorPullOneAndUpdateFromConfigResp) error { + result0, err := s.Impl.PullOneAndUpdateFromConfig(context.Background(), args.Repo) + if err != nil { + return err + } + *resp = ReposExecutorPullOneAndUpdateFromConfigResp{ + Result0: result0, + } + return nil +} diff --git a/internal/build/safe_common.go b/internal/build/repos_executor.go similarity index 62% rename from internal/build/safe_common.go rename to internal/build/repos_executor.go index 33cb66f..4336a0b 100644 --- a/internal/build/safe_common.go +++ b/internal/build/repos_executor.go @@ -17,24 +17,21 @@ package build import ( - "os" - "os/exec" - "strings" + "context" + + "gitea.plemya-x.ru/Plemya-x/ALR/internal/repos" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/types" ) -func setCommonCmdEnv(cmd *exec.Cmd) { - cmd.Env = []string{ - "HOME=/var/cache/alr", - "LOGNAME=alr", - "USER=alr", - "PATH=/usr/bin:/bin:/usr/local/bin", - } - for _, env := range os.Environ() { - if strings.HasPrefix(env, "LANG=") || - strings.HasPrefix(env, "LANGUAGE=") || - strings.HasPrefix(env, "LC_") || - strings.HasPrefix(env, "ALR_LOG_LEVEL=") { - cmd.Env = append(cmd.Env, env) - } - } +type reposExecutor struct{ r *repos.Repos } + +func NewRepos(r *repos.Repos) ReposExecutor { + return &reposExecutor{r} +} + +func (r *reposExecutor) PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) (types.Repo, error) { + if err := r.r.PullOneAndUpdateFromConfig(ctx, repo); err != nil { + return *repo, err + } + return *repo, nil } 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/repos/pull.go b/internal/repos/pull.go index e0944d8..f0da904 100644 --- a/internal/repos/pull.go +++ b/internal/repos/pull.go @@ -68,7 +68,7 @@ func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error { } for _, repo := range repos { - err := rs.pullRepo(ctx, repo) + err := rs.pullRepo(ctx, &repo, false) if err != nil { return err } @@ -77,7 +77,16 @@ func (rs *Repos) Pull(ctx context.Context, repos []types.Repo) error { return nil } -func (rs *Repos) pullRepo(ctx context.Context, repo types.Repo) error { +func (rs *Repos) PullOneAndUpdateFromConfig(ctx context.Context, repo *types.Repo) error { + err := rs.pullRepo(ctx, repo, true) + if err != nil { + return err + } + + return nil +} + +func (rs *Repos) pullRepo(ctx context.Context, repo *types.Repo, updateRepoFromToml bool) error { urls := []string{repo.URL} urls = append(urls, repo.Mirrors...) @@ -88,7 +97,7 @@ func (rs *Repos) pullRepo(ctx context.Context, repo types.Repo) error { slog.Info(gotext.Get("Trying mirror"), "repo", repo.Name, "mirror", repoURL) } - err := rs.pullRepoFromURL(ctx, repoURL, repo) + err := rs.pullRepoFromURL(ctx, repoURL, repo, updateRepoFromToml) if err != nil { lastErr = err slog.Warn(gotext.Get("Failed to pull from URL"), "repo", repo.Name, "url", repoURL, "error", err) @@ -149,7 +158,7 @@ func readGitRepo(repoDir, repoUrl string) (*git.Repository, bool, error) { return r, true, nil } -func (rs *Repos) pullRepoFromURL(ctx context.Context, rawRepoUrl string, repo types.Repo) error { +func (rs *Repos) pullRepoFromURL(ctx context.Context, rawRepoUrl string, repo *types.Repo, update bool) error { repoURL, err := url.Parse(rawRepoUrl) if err != nil { return fmt.Errorf("invalid URL %s: %w", rawRepoUrl, err) @@ -214,12 +223,12 @@ func (rs *Repos) pullRepoFromURL(ctx context.Context, rawRepoUrl string, repo ty // empty. In this case, we need to update the DB fully // rather than just incrementally. if rs.db.IsEmpty() || freshGit { - err = rs.processRepoFull(ctx, repo, repoDir) + err = rs.processRepoFull(ctx, *repo, repoDir) if err != nil { return err } } else { - err = rs.processRepoChanges(ctx, repo, r, w, old, new) + err = rs.processRepoChanges(ctx, *repo, r, w, old, new) if err != nil { return err } @@ -247,6 +256,18 @@ func (rs *Repos) pullRepoFromURL(ctx context.Context, rawRepoUrl string, repo ty } } + if update { + if repoCfg.Repo.URL != "" { + repo.URL = repoCfg.Repo.URL + } + if repoCfg.Repo.Ref != "" { + repo.Ref = repoCfg.Repo.Ref + } + if len(repoCfg.Repo.Mirrors) > 0 { + repo.Mirrors = repoCfg.Repo.Mirrors + } + } + return nil } diff --git a/internal/translations/default.pot b/internal/translations/default.pot index 354adf8..687cd59 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 "" @@ -407,27 +407,27 @@ msgstr "" msgid "ERROR" msgstr "" -#: internal/repos/pull.go:88 +#: internal/repos/pull.go:97 msgid "Trying mirror" msgstr "" -#: internal/repos/pull.go:94 +#: internal/repos/pull.go:103 msgid "Failed to pull from URL" msgstr "" -#: internal/repos/pull.go:158 +#: internal/repos/pull.go:167 msgid "Pulling repository" msgstr "" -#: internal/repos/pull.go:195 +#: internal/repos/pull.go:204 msgid "Repository up to date" msgstr "" -#: internal/repos/pull.go:230 +#: internal/repos/pull.go:239 msgid "Git repository does not appear to be a valid ALR repo" msgstr "" -#: internal/repos/pull.go:246 +#: internal/repos/pull.go:255 msgid "" "ALR repo's minimum ALR version is greater than the current version. Try " "updating ALR if something doesn't work." @@ -481,11 +481,11 @@ msgstr "" msgid "Enable interactive questions and prompts" msgstr "" -#: main.go:147 +#: main.go:148 msgid "Show help" msgstr "" -#: main.go:151 +#: main.go:152 msgid "Error while running app" msgstr "" @@ -517,44 +517,44 @@ msgstr "" msgid "Pull all repositories that have changed" msgstr "" -#: repo.go:41 +#: repo.go:42 msgid "Manage repos" msgstr "" -#: repo.go:55 repo.go:625 +#: repo.go:56 repo.go:625 msgid "Remove an existing repository" msgstr "" -#: repo.go:57 repo.go:521 +#: repo.go:58 repo.go:521 msgid "" msgstr "" -#: repo.go:102 repo.go:465 repo.go:568 +#: repo.go:103 repo.go:465 repo.go:568 msgid "Repo \"%s\" does not exist" msgstr "" -#: repo.go:109 +#: repo.go:110 msgid "Error removing repo directory" msgstr "" -#: repo.go:113 repo.go:180 repo.go:253 repo.go:316 repo.go:389 repo.go:504 +#: repo.go:114 repo.go:195 repo.go:253 repo.go:316 repo.go:389 repo.go:504 #: repo.go:576 msgid "Error saving config" msgstr "" -#: repo.go:132 +#: repo.go:133 msgid "Error removing packages from database" msgstr "" -#: repo.go:143 repo.go:595 +#: repo.go:144 repo.go:595 msgid "Add a new repository" msgstr "" -#: repo.go:144 repo.go:270 repo.go:345 repo.go:402 +#: repo.go:145 repo.go:270 repo.go:345 repo.go:402 msgid " " msgstr "" -#: repo.go:169 +#: repo.go:170 msgid "Repo \"%s\" already exists" msgstr "" diff --git a/internal/translations/po/ru/default.po b/internal/translations/po/ru/default.po index f2c9932..6d9ba8f 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 "Установка зависимостей" @@ -421,27 +421,27 @@ msgstr "" msgid "ERROR" msgstr "ОШИБКА" -#: internal/repos/pull.go:88 +#: internal/repos/pull.go:97 msgid "Trying mirror" msgstr "Пробую зеркало" -#: internal/repos/pull.go:94 +#: internal/repos/pull.go:103 msgid "Failed to pull from URL" msgstr "Не удалось извлечь из URL" -#: internal/repos/pull.go:158 +#: internal/repos/pull.go:167 msgid "Pulling repository" msgstr "Скачивание репозитория" -#: internal/repos/pull.go:195 +#: internal/repos/pull.go:204 msgid "Repository up to date" msgstr "Репозиторий уже обновлён" -#: internal/repos/pull.go:230 +#: internal/repos/pull.go:239 msgid "Git repository does not appear to be a valid ALR repo" msgstr "Репозиторий Git не поддерживается репозиторием ALR" -#: internal/repos/pull.go:246 +#: internal/repos/pull.go:255 msgid "" "ALR repo's minimum ALR version is greater than the current version. Try " "updating ALR if something doesn't work." @@ -497,11 +497,11 @@ msgstr "Аргументы, которые будут переданы мене msgid "Enable interactive questions and prompts" msgstr "Включение интерактивных вопросов и запросов" -#: main.go:147 +#: main.go:148 msgid "Show help" msgstr "Показать справку" -#: main.go:151 +#: main.go:152 msgid "Error while running app" msgstr "Ошибка при запуске приложения" @@ -533,44 +533,44 @@ msgstr "%s %s загружается — %s/с\n" msgid "Pull all repositories that have changed" msgstr "Скачать все изменённые репозитории" -#: repo.go:41 +#: repo.go:42 msgid "Manage repos" msgstr "Управление репозиториями" -#: repo.go:55 repo.go:625 +#: repo.go:56 repo.go:625 msgid "Remove an existing repository" msgstr "Удалить существующий репозиторий" -#: repo.go:57 repo.go:521 +#: repo.go:58 repo.go:521 msgid "" msgstr "<имя>" -#: repo.go:102 repo.go:465 repo.go:568 +#: repo.go:103 repo.go:465 repo.go:568 msgid "Repo \"%s\" does not exist" msgstr "Репозитория \"%s\" не существует" -#: repo.go:109 +#: repo.go:110 msgid "Error removing repo directory" msgstr "Ошибка при удалении каталога репозитория" -#: repo.go:113 repo.go:180 repo.go:253 repo.go:316 repo.go:389 repo.go:504 +#: repo.go:114 repo.go:195 repo.go:253 repo.go:316 repo.go:389 repo.go:504 #: repo.go:576 msgid "Error saving config" msgstr "Ошибка при сохранении конфигурации" -#: repo.go:132 +#: repo.go:133 msgid "Error removing packages from database" msgstr "Ошибка при удалении пакетов из базы данных" -#: repo.go:143 repo.go:595 +#: repo.go:144 repo.go:595 msgid "Add a new repository" msgstr "Добавить новый репозиторий" -#: repo.go:144 repo.go:270 repo.go:345 repo.go:402 +#: repo.go:145 repo.go:270 repo.go:345 repo.go:402 msgid " " msgstr "<имя> " -#: repo.go:169 +#: repo.go:170 msgid "Repo \"%s\" already exists" msgstr "Репозиторий \"%s\" уже существует" diff --git a/internal/utils/cmd.go b/internal/utils/cmd.go index 5753f2f..3b4025f 100644 --- a/internal/utils/cmd.go +++ b/internal/utils/cmd.go @@ -131,11 +131,11 @@ func EnsureIsAlrUser() error { } newUid := syscall.Getuid() if newUid != uid { - return errors.New("new uid don't matches requested") + return errors.New("uid don't matches requested") } newGid := syscall.Getgid() if newGid != gid { - return errors.New("new gid don't matches requested") + return errors.New("gid don't matches requested") } return nil } diff --git a/main.go b/main.go index af84885..368ec3d 100644 --- a/main.go +++ b/main.go @@ -88,6 +88,7 @@ func GetApp() *cli.App { InternalBuildCmd(), InternalInstallCmd(), InternalMountCmd(), + InternalReposCmd(), }, Before: func(c *cli.Context) error { if trimmed := strings.TrimSpace(c.String("pm-args")); trimmed != "" { diff --git a/pkg/dl/git.go b/pkg/dl/git.go index b5380b1..26f8de5 100644 --- a/pkg/dl/git.go +++ b/pkg/dl/git.go @@ -22,6 +22,7 @@ package dl import ( "context" "errors" + "fmt" "net/url" "path" "strconv" @@ -149,6 +150,7 @@ func (d *GitDownloader) Update(opts Options) (bool, error) { u.Scheme = strings.TrimPrefix(u.Scheme, "git+") query := u.Query() + rev := query.Get("~rev") query.Del("~rev") depthStr := query.Get("~depth") @@ -177,27 +179,67 @@ func (d *GitDownloader) Update(opts Options) (bool, error) { } } - po := &git.PullOptions{ - Depth: depth, - Progress: opts.Progress, - RecurseSubmodules: git.NoRecurseSubmodules, - } - - if recursive == "true" { - po.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth + // First, we do a fetch to get all the revisions. + fo := &git.FetchOptions{ + Depth: depth, + Progress: opts.Progress, } m, err := getManifest(opts.Destination) manifestOK := err == nil - err = w.Pull(po) - if err != nil { - if errors.Is(err, git.NoErrAlreadyUpToDate) { - return false, nil - } + err = r.Fetch(fo) + if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { return false, err } + // If a revision is specified, switch to it. + if rev != "" { + // We are trying to find the revision as a hash of the commit + hash, err := r.ResolveRevision(plumbing.Revision(rev)) + if err != nil { + return false, fmt.Errorf("failed to resolve revision %s: %w", rev, err) + } + + err = w.Checkout(&git.CheckoutOptions{ + Hash: *hash, + }) + if err != nil { + return false, fmt.Errorf("failed to checkout revision %s: %w", rev, err) + } + + if recursive == "true" { + submodules, err := w.Submodules() + if err == nil { + err = submodules.Update(&git.SubmoduleUpdateOptions{ + Init: true, + }) + if err != nil { + return false, fmt.Errorf("failed to update submodules %s: %w", rev, err) + } + } + } + } else { + // If the revision is not specified, we do a regular pull. + po := &git.PullOptions{ + Depth: depth, + Progress: opts.Progress, + RecurseSubmodules: git.NoRecurseSubmodules, + } + + if recursive == "true" { + po.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth + } + + err = w.Pull(po) + if err != nil { + if errors.Is(err, git.NoErrAlreadyUpToDate) { + return false, nil + } + return false, err + } + } + err = VerifyHashFromLocal("", opts) if err != nil { return false, err diff --git a/pkg/dl/git_test.go b/pkg/dl/git_test.go index 1c65db4..eaf3fbf 100644 --- a/pkg/dl/git_test.go +++ b/pkg/dl/git_test.go @@ -153,7 +153,7 @@ func TestGitDownloaderUpdate(t *testing.T) { assert.NoError(t, err) updated, err := d.Update(dl.Options{ - URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git~rev=test-update-git-downloader", + URL: "git+https://gitea.plemya-x.ru/Plemya-x/repo-for-tests.git?~rev=test-update-git-downloader", Destination: dest, Hash: hsh, HashAlgorithm: "sha256", diff --git a/pkg/types/repo.go b/pkg/types/repo.go index 43ef92d..4c6ad48 100644 --- a/pkg/types/repo.go +++ b/pkg/types/repo.go @@ -22,6 +22,9 @@ package types // RepoConfig represents a ALR repo's alr-repo.toml file. type RepoConfig struct { Repo struct { - MinVersion string `toml:"minVersion"` + MinVersion string `toml:"minVersion"` + URL string `toml:"url"` + Ref string `toml:"ref"` + Mirrors []string `toml:"mirrors"` } } diff --git a/repo.go b/repo.go index 7fc9e4b..5669e5f 100644 --- a/repo.go +++ b/repo.go @@ -29,6 +29,7 @@ import ( "github.com/urfave/cli/v2" "golang.org/x/exp/slices" + "gitea.plemya-x.ru/Plemya-x/ALR/internal/build" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" "gitea.plemya-x.ru/Plemya-x/ALR/internal/utils" @@ -169,10 +170,24 @@ func AddRepoCmd() *cli.Command { return cliutils.FormatCliExit(gotext.Get("Repo \"%s\" already exists", repo.Name), nil) } } - reposSlice = append(reposSlice, types.Repo{ + + newRepo := types.Repo{ Name: name, URL: repoURL, - }) + } + + r, close, err := build.GetSafeReposExecutor() + if err != nil { + return err + } + defer close() + + newRepo, err = r.PullOneAndUpdateFromConfig(c.Context, &newRepo) + if err != nil { + return err + } + + reposSlice = append(reposSlice, newRepo) cfg.SetRepos(reposSlice) err = cfg.System.Save() @@ -180,21 +195,6 @@ func AddRepoCmd() *cli.Command { return cliutils.FormatCliExit(gotext.Get("Error saving config"), err) } - if err := utils.ExitIfCantDropCapsToAlrUserNoPrivs(); err != nil { - return err - } - - deps, err = appbuilder. - New(ctx). - UseConfig(cfg). - WithDB(). - WithReposForcePull(). - Build() - if err != nil { - return err - } - defer deps.Defer() - return nil }), }