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)
+ }
+}