From c4a92c67d4df82d1c21f8d0641b6e999ffff4e6d Mon Sep 17 00:00:00 2001 From: Maxim Slipenko Date: Sun, 22 Jun 2025 12:44:21 +0300 Subject: [PATCH] fix parsing overrides --- Makefile | 1 + assets/coverage-badge.svg | 4 +- generators/alrsh-package/main.go | 251 +++++++++++++++++++++++ go.mod | 3 +- go.sum | 2 + info.go | 44 ++-- internal/build/build.go | 2 +- internal/shutils/decoder/decoder.go | 192 +++++++++-------- internal/shutils/decoder/decoder_test.go | 57 +++-- internal/translations/default.pot | 4 +- internal/translations/po/ru/default.po | 4 +- pkg/alrsh/overridable.go | 45 ++++ pkg/alrsh/package.go | 132 ++++++++---- pkg/alrsh/package_gen.go | 105 ++++++++++ pkg/alrsh/view.go | 37 ++++ 15 files changed, 705 insertions(+), 178 deletions(-) create mode 100644 generators/alrsh-package/main.go create mode 100644 pkg/alrsh/package_gen.go create mode 100644 pkg/alrsh/view.go diff --git a/Makefile b/Makefile index 8b1d5aa..406213c 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,7 @@ build: check-no-root $(BIN) export CGO_ENABLED := 0 $(BIN): + go generate ./... go build -ldflags="-X 'gitea.plemya-x.ru/Plemya-x/ALR/internal/config.Version=$(GIT_VERSION)'" -o $@ check-no-root: diff --git a/assets/coverage-badge.svg b/assets/coverage-badge.svg index ff1ebbf..b7b7768 100644 --- a/assets/coverage-badge.svg +++ b/assets/coverage-badge.svg @@ -11,7 +11,7 @@ coverage coverage - 20.1% - 20.1% + 19.7% + 19.7% diff --git a/generators/alrsh-package/main.go b/generators/alrsh-package/main.go new file mode 100644 index 0000000..614a423 --- /dev/null +++ b/generators/alrsh-package/main.go @@ -0,0 +1,251 @@ +// 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" + "reflect" + "strings" + "text/template" +) + +func resolvedStructGenerator(buf *bytes.Buffer, fields []*ast.Field) { + contentTemplate := template.Must(template.New("").Parse(` +type {{ .EntityNameLower }}Resolved struct { +{{ .StructFields }} +} +`)) + + var structFieldsBuilder strings.Builder + + for _, field := range fields { + for _, name := range field.Names { + // Поле с типом + fieldTypeStr := exprToString(field.Type) + + // Структура поля + var buf bytes.Buffer + buf.WriteString("\t") + buf.WriteString(name.Name) + buf.WriteString(" ") + buf.WriteString(fieldTypeStr) + + // Обработка json-тега + jsonTag := "" + if field.Tag != nil { + raw := strings.Trim(field.Tag.Value, "`") + tag := reflect.StructTag(raw) + if val := tag.Get("json"); val != "" { + jsonTag = val + } + } + if jsonTag == "" { + jsonTag = strings.ToLower(name.Name) + } + buf.WriteString(fmt.Sprintf(" `json:\"%s\"`", jsonTag)) + buf.WriteString("\n") + structFieldsBuilder.Write(buf.Bytes()) + } + } + + params := struct { + EntityNameLower string + StructFields string + }{ + EntityNameLower: "package", + StructFields: structFieldsBuilder.String(), + } + + err := contentTemplate.Execute(buf, params) + if err != nil { + log.Fatalf("execute template: %v", err) + } +} + +func toResolvedFuncGenerator(buf *bytes.Buffer, fields []*ast.Field) { + contentTemplate := template.Must(template.New("").Parse(` +func {{ .EntityName }}ToResolved(src *{{ .EntityName }}) {{ .EntityNameLower }}Resolved { + return {{ .EntityNameLower }}Resolved{ +{{ .Assignments }} + } +} +`)) + + var assignmentsBuilder strings.Builder + + for _, field := range fields { + for _, name := range field.Names { + var assignBuf bytes.Buffer + assignBuf.WriteString("\t\t") + assignBuf.WriteString(name.Name) + assignBuf.WriteString(": ") + if isOverridableField(field.Type) { + assignBuf.WriteString(fmt.Sprintf("src.%s.Resolved()", name.Name)) + } else { + assignBuf.WriteString(fmt.Sprintf("src.%s", name.Name)) + } + assignBuf.WriteString(",\n") + assignmentsBuilder.Write(assignBuf.Bytes()) + } + } + + params := struct { + EntityName string + EntityNameLower string + Assignments string + }{ + EntityName: "Package", + EntityNameLower: "package", + Assignments: assignmentsBuilder.String(), + } + + err := contentTemplate.Execute(buf, params) + if err != nil { + log.Fatalf("execute template: %v", err) + } +} + +func resolveFuncGenerator(buf *bytes.Buffer, fields []*ast.Field) { + contentTemplate := template.Must(template.New("").Parse(` +func Resolve{{ .EntityName }}(pkg *{{ .EntityName }}, overrides []string) { +{{.Code}}} +`)) + + var codeBuilder strings.Builder + + for _, field := range fields { + for _, name := range field.Names { + if isOverridableField(field.Type) { + var buf bytes.Buffer + buf.WriteString(fmt.Sprintf("\t\tpkg.%s.Resolve(overrides)\n", name.Name)) + codeBuilder.Write(buf.Bytes()) + } + } + } + + params := struct { + EntityName string + Code string + }{ + EntityName: "Package", + Code: codeBuilder.String(), + } + + err := contentTemplate.Execute(buf, params) + if err != nil { + log.Fatalf("execute template: %v", err) + } +} + +func main() { + path := os.Getenv("GOFILE") + if path == "" { + log.Fatal("GOFILE must be set") + } + + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, path, nil, parser.AllErrors) + if err != nil { + log.Fatalf("parsing file: %v", err) + } + + entityName := "Package" // имя структуры, которую анализируем + + found := false + + fields := make([]*ast.Field, 0) + + // Ищем структуру с нужным именем + 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) + if typeSpec.Name.Name != entityName { + continue + } + structType, ok := typeSpec.Type.(*ast.StructType) + if !ok { + continue + } + fields = structType.Fields.List + found = true + } + } + + if !found { + log.Fatalf("struct %s not found", entityName) + } + + var buf bytes.Buffer + + buf.WriteString("// DO NOT EDIT MANUALLY. This file is generated.\n") + buf.WriteString("package alrsh") + + resolvedStructGenerator(&buf, fields) + toResolvedFuncGenerator(&buf, fields) + resolveFuncGenerator(&buf, fields) + + // Форматируем вывод + 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 exprToString(expr ast.Expr) string { + if t, ok := expr.(*ast.IndexExpr); ok { + if ident, ok := t.X.(*ast.Ident); ok && ident.Name == "OverridableField" { + return exprToString(t.Index) // T + } + } + + var buf bytes.Buffer + if err := format.Node(&buf, token.NewFileSet(), expr); err != nil { + return "" + } + return buf.String() +} + +func isOverridableField(expr ast.Expr) bool { + indexExpr, ok := expr.(*ast.IndexExpr) + if !ok { + return false + } + ident, ok := indexExpr.X.(*ast.Ident) + return ok && ident.Name == "OverridableField" +} diff --git a/go.mod b/go.mod index 28c6ee5..52c4f46 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/efficientgo/e2e v0.14.1-0.20240418111536-97db25a0c6c0 github.com/go-git/go-billy/v5 v5.6.0 github.com/go-git/go-git/v5 v5.13.0 + github.com/goccy/go-yaml v1.18.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/goreleaser/nfpm/v2 v2.41.0 github.com/hashicorp/go-hclog v0.14.1 @@ -38,7 +39,6 @@ require ( golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/sys v0.31.0 golang.org/x/text v0.23.0 - gopkg.in/yaml.v3 v3.0.1 modernc.org/sqlite v1.25.0 mvdan.cc/sh/v3 v3.10.0 xorm.io/xorm v1.3.9 @@ -139,6 +139,7 @@ require ( google.golang.org/grpc v1.58.3 // indirect google.golang.org/protobuf v1.36.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect lukechampine.com/uint128 v1.2.0 // indirect modernc.org/cc/v3 v3.40.0 // indirect modernc.org/ccgo/v3 v3.16.13 // indirect diff --git a/go.sum b/go.sum index 743c5e1..61b5bb2 100644 --- a/go.sum +++ b/go.sum @@ -164,6 +164,8 @@ github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/goccy/go-json v0.8.1 h1:4/Wjm0JIJaTDm8K1KcGrLHJoa8EsJ13YWeX+6Kfq6uI= github.com/goccy/go-json v0.8.1/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= diff --git a/info.go b/info.go index 5652eb5..31e6c9f 100644 --- a/info.go +++ b/info.go @@ -23,10 +23,10 @@ import ( "fmt" "os" + "github.com/goccy/go-yaml" "github.com/jeandeaual/go-locale" "github.com/leonelquinteros/gotext" "github.com/urfave/cli/v2" - "gopkg.in/yaml.v3" "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils" appbuilder "gitea.plemya-x.ru/Plemya-x/ALR/internal/cliutils/app_builder" @@ -121,35 +121,27 @@ func InfoCmd() *cli.Command { systemLang = "en" } - if !all { - info, err := distro.ParseOSRelease(ctx) - if err != nil { - return cliutils.FormatCliExit(gotext.Get("Error parsing os-release file"), err) - } - names, err = overrides.Resolve( - info, - overrides.DefaultOpts. - WithLanguages([]string{systemLang}), - ) - if err != nil { - return cliutils.FormatCliExit(gotext.Get("Error resolving overrides"), err) - } + info, err := distro.ParseOSRelease(ctx) + if err != nil { + return cliutils.FormatCliExit(gotext.Get("Error parsing os-release file"), err) + } + names, err = overrides.Resolve( + info, + overrides.DefaultOpts. + WithLanguages([]string{systemLang}), + ) + if err != nil { + return cliutils.FormatCliExit(gotext.Get("Error resolving overrides"), err) } for _, pkg := range pkgs { - if !all { - alrsh.ResolvePackage(&pkg, names) - err = yaml.NewEncoder(os.Stdout).Encode(pkg) - if err != nil { - return cliutils.FormatCliExit(gotext.Get("Error encoding script variables"), err) - } - } else { - err = yaml.NewEncoder(os.Stdout).Encode(pkg) - if err != nil { - return cliutils.FormatCliExit(gotext.Get("Error encoding script variables"), err) - } + alrsh.ResolvePackage(&pkg, names) + view := alrsh.NewPackageView(pkg) + view.Resolved = !all + err = yaml.NewEncoder(os.Stdout, yaml.UseJSONMarshaler(), yaml.OmitEmpty()).Encode(view) + if err != nil { + return cliutils.FormatCliExit(gotext.Get("Error encoding script variables"), err) } - fmt.Println("---") } diff --git a/internal/build/build.go b/internal/build/build.go index ff7a2d2..fe23276 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -367,7 +367,7 @@ func (b *Builder) BuildPackage( } slog.Debug("ViewScript") - slog.Debug("", "varsOfPackages", varsOfPackages) + slog.Debug("", "varsOfPackages", varsOfPackages[0]) err = b.scriptViewerExecutor.ViewScript(ctx, input, sf, basePkg) if err != nil { return nil, err diff --git a/internal/shutils/decoder/decoder.go b/internal/shutils/decoder/decoder.go index 0119664..d61277b 100644 --- a/internal/shutils/decoder/decoder.go +++ b/internal/shutils/decoder/decoder.go @@ -23,7 +23,6 @@ import ( "context" "errors" "fmt" - "log/slog" "reflect" "strings" @@ -75,75 +74,99 @@ func New(info *distro.OSRelease, runner *interp.Runner) *Decoder { // DecodeVar decodes a variable to val using reflection. // Structs should use the "sh" struct tag. func (d *Decoder) DecodeVar(name string, val any) error { - variable := d.getVar(name) - if variable == nil { - return VarNotFoundError{name} - } + origType := reflect.TypeOf(val).Elem() + isOverridableField := strings.Contains(origType.String(), "OverridableField[") - dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ - WeaklyTypedInput: true, - DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) { - if strings.Contains(to.Type().String(), "alrsh.OverridableField") { - if to.Kind() != reflect.Ptr && to.CanAddr() { - to = to.Addr() - } + if !isOverridableField { + variable := d.getVarNoOverrides(name) + if variable == nil { + return VarNotFoundError{name} + } - names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name)) - if err != nil { - return nil, err - } - - isNotSet := true - - setMethod := to.MethodByName("Set") - setResolvedMethod := to.MethodByName("SetResolved") - - for _, varName := range names { - val := d.getVarNoOverrides(varName) - if val == nil { - continue + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + WeaklyTypedInput: true, + Result: val, // передаем указатель на новое значение + TagName: "sh", + DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) { + if from.Kind() == reflect.Slice && to.Kind() == reflect.String { + s, ok := from.Interface().([]string) + if ok && len(s) == 1 { + return s[0], nil } - - t := setMethod.Type().In(1) - - newVal := from - - if !newVal.Type().AssignableTo(t) { - newVal = reflect.New(t) - err = d.DecodeVar(name, newVal.Interface()) - if err != nil { - return nil, err - } - newVal = newVal.Elem() - } - - if isNotSet { - setResolvedMethod.Call([]reflect.Value{newVal}) - } - - override := strings.TrimPrefix(strings.TrimPrefix(varName, name), "_") - setMethod.Call([]reflect.Value{reflect.ValueOf(override), newVal}) } + return from.Interface(), nil + }), + }) + if err != nil { + return err + } - return to, nil + switch variable.Kind { + case expand.Indexed: + return dec.Decode(variable.List) + case expand.Associative: + return dec.Decode(variable.Map) + default: + return dec.Decode(variable.Str) + } + } else { + vars := d.getVarsByPrefix(name) + + if len(vars) == 0 { + return VarNotFoundError{name} + } + + reflectVal := reflect.ValueOf(val) + overridableVal := reflect.ValueOf(val).Elem() + + dataField := overridableVal.FieldByName("data") + if !dataField.IsValid() { + return fmt.Errorf("data field not found in OverridableField") + } + mapType := dataField.Type() // map[string]T + elemType := mapType.Elem() // T + + var overridablePtr reflect.Value + if reflectVal.Kind() == reflect.Ptr { + overridablePtr = reflectVal + } else { + if !reflectVal.CanAddr() { + return fmt.Errorf("OverridableField value is not addressable") } - return from.Interface(), nil - }), - Result: val, - TagName: "sh", - }) - if err != nil { - slog.Warn("err", "err", err) - return err - } + overridablePtr = reflectVal.Addr() + } - switch variable.Kind { - case expand.Indexed: - return dec.Decode(variable.List) - case expand.Associative: - return dec.Decode(variable.Map) - default: - return dec.Decode(variable.Str) + setValue := overridablePtr.MethodByName("Set") + if !setValue.IsValid() { + return fmt.Errorf("method Set not found on OverridableField") + } + + for _, v := range vars { + varName := v.Name + + key := strings.TrimPrefix(strings.TrimPrefix(varName, name), "_") + newVal := reflect.New(elemType) + + if err := d.DecodeVar(varName, newVal.Interface()); err != nil { + return err + } + + keyValue := reflect.ValueOf(key) + setValue.Call([]reflect.Value{keyValue, newVal.Elem()}) + } + + resolveValue := overridablePtr.MethodByName("Resolve") + if !resolveValue.IsValid() { + return fmt.Errorf("method Resolve not found on OverridableField") + } + + names, err := overrides.Resolve(d.info, overrides.DefaultOpts) + if err != nil { + return err + } + + resolveValue.Call([]reflect.Value{reflect.ValueOf(names)}) + return nil } } @@ -284,23 +307,6 @@ func (d *Decoder) getFunc(name string) *syntax.Stmt { return nil } -// getVar gets a variable based on its name, taking into account -// override variables and nameref variables. -func (d *Decoder) getVar(name string) *expand.Variable { - names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name)) - if err != nil { - return nil - } - - for _, varName := range names { - res := d.getVarNoOverrides(varName) - if res != nil { - return res - } - } - return nil -} - func (d *Decoder) getVarNoOverrides(name string) *expand.Variable { val, ok := d.Runner.Vars[name] if ok { @@ -318,6 +324,32 @@ func (d *Decoder) getVarNoOverrides(name string) *expand.Variable { return nil } +type vars struct { + Name string + Value *expand.Variable +} + +func (d *Decoder) getVarsByPrefix(prefix string) []*vars { + result := make([]*vars, 0) + for name, val := range d.Runner.Vars { + if !strings.HasPrefix(name, prefix) { + continue + } + switch prefix { + case "auto_req": + if strings.HasPrefix(name, "auto_req_skiplist") { + continue + } + case "auto_prov": + if strings.HasPrefix(name, "auto_prov_skiplist") { + continue + } + } + result = append(result, &vars{name, &val}) + } + return result +} + func IsTruthy(value string) bool { value = strings.ToLower(strings.TrimSpace(value)) return value == "true" || value == "yes" || value == "1" diff --git a/internal/shutils/decoder/decoder_test.go b/internal/shutils/decoder/decoder_test.go index 54a2b38..149faef 100644 --- a/internal/shutils/decoder/decoder_test.go +++ b/internal/shutils/decoder/decoder_test.go @@ -32,24 +32,25 @@ import ( "mvdan.cc/sh/v3/syntax" "gitea.plemya-x.ru/Plemya-x/ALR/internal/shutils/decoder" + "gitea.plemya-x.ru/Plemya-x/ALR/pkg/alrsh" "gitea.plemya-x.ru/Plemya-x/ALR/pkg/distro" ) type BuildVars struct { - Name string `sh:"name,required"` - Version string `sh:"version,required"` - Release int `sh:"release,required"` - Epoch uint `sh:"epoch"` - Description string `sh:"desc"` - Homepage string `sh:"homepage"` - Maintainer string `sh:"maintainer"` - Architectures []string `sh:"architectures"` - Licenses []string `sh:"license"` - Provides []string `sh:"provides"` - Conflicts []string `sh:"conflicts"` - Depends []string `sh:"deps"` - BuildDepends []string `sh:"build_deps"` - Replaces []string `sh:"replaces"` + Name string `sh:"name,required"` + Version string `sh:"version,required"` + Release int `sh:"release,required"` + Epoch uint `sh:"epoch"` + Description alrsh.OverridableField[string] `sh:"desc"` + Homepage string `sh:"homepage"` + Maintainer string `sh:"maintainer"` + Architectures []string `sh:"architectures"` + Licenses []string `sh:"license"` + Provides []string `sh:"provides"` + Conflicts []string `sh:"conflicts"` + Depends []string `sh:"deps"` + BuildDepends alrsh.OverridableField[[]string] `sh:"build_deps"` + Replaces alrsh.OverridableField[[]string] `sh:"replaces"` } const testScript = ` @@ -113,22 +114,34 @@ func TestDecodeVars(t *testing.T) { } expected := BuildVars{ - Name: "test", - Version: "0.0.1", - Release: 1, - Epoch: 2, - Description: "Test package", + Name: "test", + Version: "0.0.1", + Release: 1, + Epoch: 2, + Description: alrsh.OverridableFromMap(map[string]string{ + "": "Test package", + }), Homepage: "https://gitea.plemya-x.ru/xpamych/ALR", Maintainer: "Евгений Храмов ", Architectures: []string{"arm64", "amd64"}, Licenses: []string{"GPL-3.0-or-later"}, Provides: []string{"test"}, Conflicts: []string{"test"}, - Replaces: []string{"test-legacy"}, - Depends: []string{"sudo"}, - BuildDepends: []string{"go"}, + Replaces: alrsh.OverridableFromMap(map[string][]string{ + "": {"test-old"}, + "test_os": {"test-legacy"}, + }), + Depends: []string{"sudo"}, + BuildDepends: alrsh.OverridableFromMap(map[string][]string{ + "": {"golang"}, + "arch": {"go"}, + }), } + expected.Description.SetResolved("Test package") + expected.Replaces.SetResolved([]string{"test-legacy"}) + expected.BuildDepends.SetResolved([]string{"go"}) + if !reflect.DeepEqual(bv, expected) { t.Errorf("Expected %v, got %v", expected, bv) } diff --git a/internal/translations/default.pot b/internal/translations/default.pot index cce76d6..50b506e 100644 --- a/internal/translations/default.pot +++ b/internal/translations/default.pot @@ -138,11 +138,11 @@ msgstr "" msgid "Can't detect system language" msgstr "" -#: info.go:135 +#: info.go:134 msgid "Error resolving overrides" msgstr "" -#: info.go:144 info.go:149 +#: info.go:143 msgid "Error encoding script variables" msgstr "" diff --git a/internal/translations/po/ru/default.po b/internal/translations/po/ru/default.po index 9866102..f5f847f 100644 --- a/internal/translations/po/ru/default.po +++ b/internal/translations/po/ru/default.po @@ -145,11 +145,11 @@ msgstr "Ошибка при поиске пакетов" msgid "Can't detect system language" msgstr "Ошибка при определении языка системы" -#: info.go:135 +#: info.go:134 msgid "Error resolving overrides" msgstr "Ошибка устранения переорпеделений" -#: info.go:144 info.go:149 +#: info.go:143 msgid "Error encoding script variables" msgstr "Ошибка кодирования переменных скрита" diff --git a/pkg/alrsh/overridable.go b/pkg/alrsh/overridable.go index f0ab785..a31dd24 100644 --- a/pkg/alrsh/overridable.go +++ b/pkg/alrsh/overridable.go @@ -68,10 +68,12 @@ func (o *OverridableField[T]) Resolve(overrides []string) { for _, override := range overrides { if v, ok := o.Has(override); ok { o.SetResolved(v) + return } } } +// Database serialization (JSON) func (f *OverridableField[T]) ToDB() ([]byte, error) { var data map[string]T @@ -103,6 +105,7 @@ func (f *OverridableField[T]) FromDB(data []byte) error { return nil } +// Gob serialization type overridableFieldGobPayload[T any] struct { Data map[string]T Resolved T @@ -136,6 +139,48 @@ func (f *OverridableField[T]) GobDecode(data []byte) error { return nil } +type overridableFieldJSONPayload[T any] struct { + Resolved *T `json:"resolved,omitempty,omitzero"` + Data map[string]T `json:"overrides,omitempty,omitzero"` +} + +func (f OverridableField[T]) MarshalJSON() ([]byte, error) { + data := make(map[string]T) + + for k, v := range f.data { + if k == "" { + data["default"] = v + } else { + data[k] = v + } + } + + payload := overridableFieldJSONPayload[T]{ + Data: data, + Resolved: &f.resolved, + } + + return json.Marshal(payload) +} + +func (f *OverridableField[T]) UnmarshalJSON(data []byte) error { + var payload overridableFieldJSONPayload[T] + if err := json.Unmarshal(data, &payload); err != nil { + return err + } + + if payload.Data == nil { + payload.Data = make(map[string]T) + } + + f.data = payload.Data + if payload.Resolved != nil { + f.resolved = *payload.Resolved + } + + return nil +} + func OverridableFromMap[T any](data map[string]T) OverridableField[T] { if data == nil { data = make(map[string]T) diff --git a/pkg/alrsh/package.go b/pkg/alrsh/package.go index 5674cd3..203ab83 100644 --- a/pkg/alrsh/package.go +++ b/pkg/alrsh/package.go @@ -14,9 +14,12 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//go:generate go run ../../generators/alrsh-package + package alrsh import ( + "encoding/json" "fmt" "reflect" "strings" @@ -39,38 +42,38 @@ func ParseNames(dec *decoder.Decoder) (*PackageNames, error) { } type Package struct { - Repository string `xorm:"pk 'repository'"` - Name string `xorm:"pk 'name'"` - BasePkgName string `xorm:"notnull 'basepkg_name'"` + Repository string `xorm:"pk 'repository'" json:"repository"` + Name string `xorm:"pk 'name'" json:"name"` + BasePkgName string `xorm:"notnull 'basepkg_name'" json:"basepkg_name"` - Version string `sh:"version" xorm:"notnull 'version'"` - Release int `sh:"release" xorm:"notnull 'release'"` - Epoch uint `sh:"epoch" xorm:"'epoch'"` - Architectures []string `sh:"architectures" xorm:"json 'architectures'"` - Licenses []string `sh:"license" xorm:"json 'licenses'"` - Provides []string `sh:"provides" xorm:"json 'provides'"` - Conflicts []string `sh:"conflicts" xorm:"json 'conflicts'"` - Replaces []string `sh:"replaces" xorm:"json 'replaces'"` + Version string `sh:"version" xorm:"notnull 'version'" json:"version"` + Release int `sh:"release" xorm:"notnull 'release'" json:"release"` + Epoch uint `sh:"epoch" xorm:"'epoch'" json:"epoch"` + Architectures []string `sh:"architectures" xorm:"json 'architectures'" json:"architectures"` + Licenses []string `sh:"license" xorm:"json 'licenses'" json:"license"` + Provides []string `sh:"provides" xorm:"json 'provides'" json:"provides"` + Conflicts []string `sh:"conflicts" xorm:"json 'conflicts'" json:"conflicts"` + Replaces []string `sh:"replaces" xorm:"json 'replaces'" json:"replaces"` - Summary OverridableField[string] `sh:"summary" xorm:"'summary'"` - Description OverridableField[string] `sh:"desc" xorm:"'description'"` - Group OverridableField[string] `sh:"group" xorm:"'group_name'"` - Homepage OverridableField[string] `sh:"homepage" xorm:"'homepage'"` - Maintainer OverridableField[string] `sh:"maintainer" xorm:"'maintainer'"` - Depends OverridableField[[]string] `sh:"deps" xorm:"'depends'"` - BuildDepends OverridableField[[]string] `sh:"build_deps" xorm:"'builddepends'"` - OptDepends OverridableField[[]string] `sh:"opt_deps" xorm:"'optdepends'"` - Sources OverridableField[[]string] `sh:"sources" xorm:"-"` - Checksums OverridableField[[]string] `sh:"checksums" xorm:"-"` - Backup OverridableField[[]string] `sh:"backup" xorm:"-"` - Scripts OverridableField[Scripts] `sh:"scripts" xorm:"-"` - AutoReq OverridableField[[]string] `sh:"auto_req" xorm:"-"` - AutoProv OverridableField[[]string] `sh:"auto_prov" xorm:"-"` - AutoReqSkipList OverridableField[[]string] `sh:"auto_req_skiplist" xorm:"-"` - AutoProvSkipList OverridableField[[]string] `sh:"auto_prov_skiplist" xorm:"-"` + Summary OverridableField[string] `sh:"summary" xorm:"'summary'" json:"summary"` + Description OverridableField[string] `sh:"desc" xorm:"'description'" json:"description"` + Group OverridableField[string] `sh:"group" xorm:"'group_name'" json:"group"` + Homepage OverridableField[string] `sh:"homepage" xorm:"'homepage'" json:"homepage"` + Maintainer OverridableField[string] `sh:"maintainer" xorm:"'maintainer'" json:"maintainer"` + Depends OverridableField[[]string] `sh:"deps" xorm:"'depends'" json:"deps"` + BuildDepends OverridableField[[]string] `sh:"build_deps" xorm:"'builddepends'" json:"build_deps"` + OptDepends OverridableField[[]string] `sh:"opt_deps" xorm:"'optdepends'" json:"opt_deps,omitempty"` + Sources OverridableField[[]string] `sh:"sources" xorm:"-" json:"sources"` + Checksums OverridableField[[]string] `sh:"checksums" xorm:"-" json:"checksums,omitempty"` + Backup OverridableField[[]string] `sh:"backup" xorm:"-" json:"backup"` + Scripts OverridableField[Scripts] `sh:"scripts" xorm:"-" json:"scripts,omitempty"` + AutoReq OverridableField[[]string] `sh:"auto_req" xorm:"-" json:"auto_req"` + AutoProv OverridableField[[]string] `sh:"auto_prov" xorm:"-" json:"auto_prov"` + AutoReqSkipList OverridableField[[]string] `sh:"auto_req_skiplist" xorm:"-" json:"auto_req_skiplist,omitempty"` + AutoProvSkipList OverridableField[[]string] `sh:"auto_prov_skiplist" xorm:"-" json:"auto_prov_skiplist,omitempty"` - FireJailed OverridableField[bool] `sh:"firejailed" xorm:"-"` - FireJailProfiles OverridableField[map[string]string] `sh:"firejail_profiles" xorm:"-"` + FireJailed OverridableField[bool] `sh:"firejailed" xorm:"-" json:"firejailed"` + FireJailProfiles OverridableField[map[string]string] `sh:"firejail_profiles" xorm:"-" json:"firejail_profiles,omitempty"` } type Scripts struct { @@ -84,25 +87,70 @@ type Scripts struct { PostTrans string `sh:"posttrans"` } -func ResolvePackage(p *Package, overrides []string) { - val := reflect.ValueOf(p).Elem() - typ := val.Type() +func (p Package) MarshalJSONWithOptions(includeOverrides bool) ([]byte, error) { + // Сначала сериализуем обычным способом для получения базовой структуры + type PackageAlias Package + baseData, err := json.Marshal(PackageAlias(p)) + if err != nil { + return nil, err + } - for i := range val.NumField() { - field := val.Field(i) - fieldType := typ.Field(i) + // Десериализуем в map для модификации + var result map[string]json.RawMessage + if err := json.Unmarshal(baseData, &result); err != nil { + return nil, err + } - if !field.CanInterface() { + // Теперь заменяем OverridableField поля + v := reflect.ValueOf(p) + t := reflect.TypeOf(p) + + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + fieldType := t.Field(i) + + jsonTag := fieldType.Tag.Get("json") + if jsonTag == "" || jsonTag == "-" { continue } - if field.Kind() == reflect.Struct && strings.HasPrefix(fieldType.Type.String(), "alrsh.OverridableField") { - of := field.Addr().Interface() - if res, ok := of.(interface { - Resolve([]string) - }); ok { - res.Resolve(overrides) + fieldName := jsonTag + if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 { + fieldName = jsonTag[:commaIdx] + } + + if field.Type().Name() == "OverridableField" || + (field.Type().Kind() == reflect.Struct && + strings.Contains(field.Type().String(), "OverridableField")) { + + fieldPtr := field.Addr() + + resolvedMethod := fieldPtr.MethodByName("Resolved") + if resolvedMethod.IsValid() { + resolved := resolvedMethod.Call(nil)[0] + + fieldData := map[string]interface{}{ + "resolved": resolved.Interface(), + } + + if includeOverrides { + allMethod := field.MethodByName("All") + if allMethod.IsValid() { + overrides := allMethod.Call(nil)[0] + if !overrides.IsNil() && overrides.Len() > 0 { + fieldData["overrides"] = overrides.Interface() + } + } + } + + fieldJSON, err := json.Marshal(fieldData) + if err != nil { + return nil, err + } + result[fieldName] = json.RawMessage(fieldJSON) } } } + + return json.Marshal(result) } diff --git a/pkg/alrsh/package_gen.go b/pkg/alrsh/package_gen.go new file mode 100644 index 0000000..834deb8 --- /dev/null +++ b/pkg/alrsh/package_gen.go @@ -0,0 +1,105 @@ +// 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 . + +// DO NOT EDIT MANUALLY. This file is generated. +package alrsh + +type packageResolved struct { + Repository string `json:"repository"` + Name string `json:"name"` + BasePkgName string `json:"basepkg_name"` + Version string `json:"version"` + Release int `json:"release"` + Epoch uint `json:"epoch"` + Architectures []string `json:"architectures"` + Licenses []string `json:"license"` + Provides []string `json:"provides"` + Conflicts []string `json:"conflicts"` + Replaces []string `json:"replaces"` + Summary string `json:"summary"` + Description string `json:"description"` + Group string `json:"group"` + Homepage string `json:"homepage"` + Maintainer string `json:"maintainer"` + Depends []string `json:"deps"` + BuildDepends []string `json:"build_deps"` + OptDepends []string `json:"opt_deps,omitempty"` + Sources []string `json:"sources"` + Checksums []string `json:"checksums,omitempty"` + Backup []string `json:"backup"` + Scripts Scripts `json:"scripts,omitempty"` + AutoReq []string `json:"auto_req"` + AutoProv []string `json:"auto_prov"` + AutoReqSkipList []string `json:"auto_req_skiplist,omitempty"` + AutoProvSkipList []string `json:"auto_prov_skiplist,omitempty"` + FireJailed bool `json:"firejailed"` + FireJailProfiles map[string]string `json:"firejail_profiles,omitempty"` +} + +func PackageToResolved(src *Package) packageResolved { + return packageResolved{ + Repository: src.Repository, + Name: src.Name, + BasePkgName: src.BasePkgName, + Version: src.Version, + Release: src.Release, + Epoch: src.Epoch, + Architectures: src.Architectures, + Licenses: src.Licenses, + Provides: src.Provides, + Conflicts: src.Conflicts, + Replaces: src.Replaces, + Summary: src.Summary.Resolved(), + Description: src.Description.Resolved(), + Group: src.Group.Resolved(), + Homepage: src.Homepage.Resolved(), + Maintainer: src.Maintainer.Resolved(), + Depends: src.Depends.Resolved(), + BuildDepends: src.BuildDepends.Resolved(), + OptDepends: src.OptDepends.Resolved(), + Sources: src.Sources.Resolved(), + Checksums: src.Checksums.Resolved(), + Backup: src.Backup.Resolved(), + Scripts: src.Scripts.Resolved(), + AutoReq: src.AutoReq.Resolved(), + AutoProv: src.AutoProv.Resolved(), + AutoReqSkipList: src.AutoReqSkipList.Resolved(), + AutoProvSkipList: src.AutoProvSkipList.Resolved(), + FireJailed: src.FireJailed.Resolved(), + FireJailProfiles: src.FireJailProfiles.Resolved(), + } +} + +func ResolvePackage(pkg *Package, overrides []string) { + pkg.Summary.Resolve(overrides) + pkg.Description.Resolve(overrides) + pkg.Group.Resolve(overrides) + pkg.Homepage.Resolve(overrides) + pkg.Maintainer.Resolve(overrides) + pkg.Depends.Resolve(overrides) + pkg.BuildDepends.Resolve(overrides) + pkg.OptDepends.Resolve(overrides) + pkg.Sources.Resolve(overrides) + pkg.Checksums.Resolve(overrides) + pkg.Backup.Resolve(overrides) + pkg.Scripts.Resolve(overrides) + pkg.AutoReq.Resolve(overrides) + pkg.AutoProv.Resolve(overrides) + pkg.AutoReqSkipList.Resolve(overrides) + pkg.AutoProvSkipList.Resolve(overrides) + pkg.FireJailed.Resolve(overrides) + pkg.FireJailProfiles.Resolve(overrides) +} diff --git a/pkg/alrsh/view.go b/pkg/alrsh/view.go new file mode 100644 index 0000000..fc176fe --- /dev/null +++ b/pkg/alrsh/view.go @@ -0,0 +1,37 @@ +// 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 alrsh + +import "encoding/json" + +type PackageView struct { + pkg Package + + Resolved bool +} + +func NewPackageView(v Package) PackageView { + return PackageView{pkg: v} +} + +func (p PackageView) MarshalJSON() ([]byte, error) { + if p.Resolved { + return json.Marshal(PackageToResolved(&p.pkg)) + } else { + return json.Marshal(p.pkg) + } +}