From 6eea1f61f89c7d728a4a154201c85b5783ce6db5 Mon Sep 17 00:00:00 2001 From: axtloss Date: Tue, 28 Nov 2023 20:07:43 +0100 Subject: [PATCH] feat: Move resolver to api/download Makes the resolve functions available in the api to let modules download their sources themselves --- core/resolver.go => api/download.go | 75 +++++++---------------------- api/structs.go | 18 +++++++ core/apt.go | 5 +- core/build.go | 59 ++++++++++++++--------- core/cmake.go | 24 +++++++-- core/compile.go | 5 +- core/dpkg-buildpackage.go | 3 +- core/dpkg.go | 3 +- core/go.go | 19 +++++++- core/loader.go | 35 +++++++++----- core/make.go | 21 +++++++- core/meson.go | 19 +++++++- core/plugins.go | 9 ++-- core/shell.go | 14 +++++- core/structs.go | 21 +------- go.mod | 2 + go.sum | 2 + 17 files changed, 201 insertions(+), 133 deletions(-) rename core/resolver.go => api/download.go (67%) diff --git a/core/resolver.go b/api/download.go similarity index 67% rename from core/resolver.go rename to api/download.go index bb8f828..f7c86e0 100644 --- a/core/resolver.go +++ b/api/download.go @@ -1,9 +1,8 @@ -package core +package api import ( "crypto/sha256" "fmt" - "github.com/vanilla-os/vib/api" "io" "net/http" "os" @@ -12,59 +11,19 @@ import ( "strings" ) -// ResolveSources resolves the sources of a recipe and downloads them -// to the downloads directory. Note that modules in this function are -// returned in the order they should be built. -func ResolveSources(recipe *Recipe) ([]Module, []api.Source, error) { - fmt.Println("Resolving sources") - - modules := GetAllModules(recipe.Modules) - //var sources []Source - - for _, module := range modules { - fmt.Printf("Resolving source for: %s\n", module.Name) - - if module.Source.URL == "" { - continue - } - - module.Source.Module = module.Name - err := DownloadSource(recipe, module.Source) - if err != nil { - return nil, nil, err - } - - sources = append(sources, module.Source) - } - - return modules, sources, nil -} - -// GetAllModules returns a list of all modules in an ordered list -func GetAllModules(modules []Module) []Module { - var orderedList []Module - - for _, module := range modules { - orderedList = append(orderedList, GetAllModules(module.Modules)...) - orderedList = append(orderedList, module) - } - - return orderedList -} - // DownloadSource downloads a source to the downloads directory // according to its type (git, tar, ...) -func DownloadSource(recipe *Recipe, source Source) error { +func DownloadSource(downloadPath string, source Source) error { fmt.Printf("Downloading source: %s\n", source.URL) if source.Type == "git" { - return DownloadGitSource(recipe, source) + return DownloadGitSource(downloadPath, source) } else if source.Type == "tar" { - err := DownloadTarSource(recipe, source) + err := DownloadTarSource(downloadPath, source) if err != nil { return err } - return checksumValidation(source, filepath.Join(recipe.DownloadsPath, source.Module)) + return checksumValidation(source, filepath.Join(downloadPath, source.Module)) } else { return fmt.Errorf("unsupported source type %s", source.Type) } @@ -72,10 +31,10 @@ func DownloadSource(recipe *Recipe, source Source) error { // DownloadGitSource downloads a git source to the downloads directory // and checks out the commit or tag -func DownloadGitSource(recipe *Recipe, source Source) error { +func DownloadGitSource(downloadPath string, source Source) error { fmt.Printf("Source is git: %s\n", source.URL) - dest := filepath.Join(recipe.DownloadsPath, source.Module) + dest := filepath.Join(downloadPath, source.Module) if source.Commit == "" && source.Tag == "" && source.Branch == "" { return fmt.Errorf("missing source commit, tag or branch") @@ -157,10 +116,10 @@ func DownloadGitSource(recipe *Recipe, source Source) error { } // DownloadTarSource downloads a tar archive to the downloads directory -func DownloadTarSource(recipe *Recipe, source Source) error { +func DownloadTarSource(downloadPath string, source Source) error { fmt.Printf("Source is tar: %s\n", source.URL) //Create the destination path - dest := filepath.Join(recipe.DownloadsPath, source.Module) + dest := filepath.Join(downloadPath, source.Module) //Download the resource res, err := http.Get(source.URL) if err != nil { @@ -186,11 +145,11 @@ func DownloadTarSource(recipe *Recipe, source Source) error { // MoveSources moves all sources from the downloads directory to the // sources directory -func MoveSources(recipe *Recipe, sources []Source) error { +func MoveSources(downloadPath string, sources []Source) error { fmt.Println("Moving sources") for _, source := range sources { - err := MoveSource(recipe, source) + err := MoveSource(downloadPath, source) if err != nil { return err } @@ -202,26 +161,26 @@ func MoveSources(recipe *Recipe, sources []Source) error { // MoveSource moves a source from the downloads directory to the // sources directory, by extracting if a tar archive or moving if a // git repository -func MoveSource(recipe *Recipe, source Source) error { +func MoveSource(downloadPath string, source Source) error { fmt.Printf("Moving source: %s\n", source.Module) if source.Type == "git" { return os.Rename( - filepath.Join(recipe.DownloadsPath, source.Module), - filepath.Join(recipe.SourcesPath, source.Module), + filepath.Join(downloadPath, source.Module), + filepath.Join(downloadPath, source.Module), ) } else if source.Type == "tar" { cmd := exec.Command( "tar", - "-xf", filepath.Join(recipe.DownloadsPath, source.Module), - "-C", recipe.SourcesPath, + "-xf", filepath.Join(downloadPath, source.Module), + "-C", downloadPath, ) err := cmd.Run() if err != nil { return err } - return os.Remove(filepath.Join(recipe.DownloadsPath, source.Module)) + return os.Remove(filepath.Join(downloadPath, source.Module)) } else { return fmt.Errorf("unsupported source type %s", source.Type) } diff --git a/api/structs.go b/api/structs.go index d1adce7..fb7540c 100644 --- a/api/structs.go +++ b/api/structs.go @@ -11,3 +11,21 @@ type Source struct { Paths []string `json:"paths"` Module string } + +type Recipe struct { + Base string `json:"base"` + Name string + Id string + SingleLayer bool `json:"singlelayer"` + Labels map[string]string `json:"labels"` + Adds map[string]string `json:"adds"` + Args map[string]string `json:"args"` + Runs []string `json:"runs"` + Cmd string `json:"cmd"` + Modules []interface{} `json:"modules"` + Path string + ParentPath string + DownloadsPath string + SourcesPath string + Containerfile string +} diff --git a/core/apt.go b/core/apt.go index c5d9bac..5b861ff 100644 --- a/core/apt.go +++ b/core/apt.go @@ -11,13 +11,14 @@ import ( type AptModule struct { Name string `json:"name"` - Type string `json:"name"` + Type string `json:"type"` Source api.Source `json:"source"` } // BuildAptModule builds a module that installs packages // using the apt package manager -func BuildAptModule(recipe *Recipe, module AptModule) (string, error) { +func BuildAptModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { + module := moduleInterface.(AptModule) if len(module.Source.Packages) > 0 { packages := "" for _, pkg := range module.Source.Packages { diff --git a/core/build.go b/core/build.go index 5e2f1f7..afcec4c 100644 --- a/core/build.go +++ b/core/build.go @@ -2,15 +2,17 @@ package core import ( "fmt" + "github.com/mitchellh/mapstructure" + "github.com/vanilla-os/vib/api" "os" ) // BuildRecipe builds a Containerfile from a recipe path -func BuildRecipe(recipePath string) (Recipe, error) { +func BuildRecipe(recipePath string) (api.Recipe, error) { // load the recipe recipe, err := LoadRecipe(recipePath) if err != nil { - return Recipe{}, err + return api.Recipe{}, err } fmt.Printf("Building recipe %s\n", recipe.Name) @@ -33,13 +35,13 @@ func BuildRecipe(recipePath string) (Recipe, error) { // in the Containerfile to build the modules cmds, err := BuildModules(recipe, recipe.Modules) if err != nil { - return Recipe{}, err + return api.Recipe{}, err } // build the Containerfile err = BuildContainerfile(recipe, cmds) if err != nil { - return Recipe{}, err + return api.Recipe{}, err } return *recipe, nil @@ -47,7 +49,7 @@ func BuildRecipe(recipePath string) (Recipe, error) { // BuildContainerfile builds a Containerfile from a recipe // and a list of modules commands -func BuildContainerfile(recipe *Recipe, cmds []ModuleCommand) error { +func BuildContainerfile(recipe *api.Recipe, cmds []ModuleCommand) error { containerfile, err := os.Create(recipe.Containerfile) if err != nil { return err @@ -172,19 +174,23 @@ func BuildContainerfile(recipe *Recipe, cmds []ModuleCommand) error { } // BuildModules builds a list of modules commands from a list of modules -func BuildModules(recipe *Recipe, modules map[string]interface{}) ([]ModuleCommand, error) { +func BuildModules(recipe *api.Recipe, modules []interface{}) ([]ModuleCommand, error) { cmds := []ModuleCommand{} + for _, moduleInterface := range modules { + var module Module + err := mapstructure.Decode(moduleInterface, &module) + if err != nil { + return nil, err + } + fmt.Printf("Creating build command for %s\n", module) - for _, module := range modules { - fmt.Printf("Creating build command for %s\n", module.(Module).Name) - - cmd, err := BuildModule(recipe, module) + cmd, err := BuildModule(recipe, moduleInterface) if err != nil { return nil, err } cmds = append(cmds, ModuleCommand{ - Name: module.(Module).Name, + Name: module.Name, Command: cmd, }) } @@ -195,25 +201,34 @@ func BuildModules(recipe *Recipe, modules map[string]interface{}) ([]ModuleComma // BuildModule builds a module command from a module // this is done by calling the appropriate module builder // function based on the module type -func BuildModule(recipe *Recipe, module interface{}) (string, error) { - switch module.(Module).Type { +func BuildModule(recipe *api.Recipe, moduleInterface interface{}) (string, error) { + var module Module + err := mapstructure.Decode(moduleInterface, &module) + if err != nil { + return "", err + } + fmt.Printf("Processing module: %s\n", module.Type) + fmt.Println(moduleInterface) + switch module.Type { case "apt": - return BuildAptModule(recipe, module.(AptModule)) + return BuildAptModule(moduleInterface, recipe) case "cmake": - return BuildCMakeModule(module.(CMakeModule)) + return BuildCMakeModule(moduleInterface, recipe) case "dpkg": - return BuildDpkgModule(module.(DpkgModule)) + return BuildDpkgModule(moduleInterface, recipe) case "dpkg-buildpackage": - return BuildDpkgBuildPkgModule(module.(DpkgBuildModule)) + return BuildDpkgBuildPkgModule(moduleInterface, recipe) case "go": - return BuildGoModule(module.(GoModule)) + return BuildGoModule(moduleInterface, recipe) case "make": - return BuildMakeModule(module.(Module)) + return BuildMakeModule(moduleInterface, recipe) case "meson": - return BuildMesonModule(module.(Module)) + return BuildMesonModule(moduleInterface, recipe) case "shell": - return BuildShellModule(module.(ShellModule)) + return BuildShellModule(moduleInterface, recipe) + case "includes": + return "", nil default: - return LoadPlugin(module.(Module).Type, module) + return LoadPlugin(module.Type, moduleInterface, recipe) } } diff --git a/core/cmake.go b/core/cmake.go index f0cb1b0..2b1980d 100644 --- a/core/cmake.go +++ b/core/cmake.go @@ -1,16 +1,30 @@ package core -import "fmt" +import ( + "fmt" + + "github.com/vanilla-os/vib/api" +) type CMakeModule struct { Name string `json:"name"` - Type string `json:"name"` - BuildVars map[string]string `json:"name"` - BuildFlags string `json:"name"` + Type string `json:"type"` + BuildVars map[string]string `json:"buildvars"` + BuildFlags string `json:"buildflags"` + Source api.Source } // BuildCMakeModule builds a module that builds a CMake project -func BuildCMakeModule(module CMakeModule) (string, error) { +func BuildCMakeModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { + module := moduleInterface.(CMakeModule) + err := api.DownloadSource(recipe.DownloadsPath, module.Source) + if err != nil { + return "", err + } + err = api.MoveSource(recipe.DownloadsPath, module.Source) + if err != nil { + return "", err + } buildVars := map[string]string{} for k, v := range module.BuildVars { buildVars[k] = v diff --git a/core/compile.go b/core/compile.go index ec17965..e0a2c9e 100644 --- a/core/compile.go +++ b/core/compile.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "github.com/vanilla-os/vib/api" "os" "os/exec" ) @@ -44,7 +45,7 @@ func CompileRecipe(recipePath string, runtime string) error { return nil } -func compileDocker(recipe Recipe, storePath string) error { +func compileDocker(recipe api.Recipe, storePath string) error { docker, err := exec.LookPath("docker") if err != nil { return err @@ -64,7 +65,7 @@ func compileDocker(recipe Recipe, storePath string) error { return cmd.Run() } -func compilePodman(recipe Recipe, storePath string) error { +func compilePodman(recipe api.Recipe, storePath string) error { podman, err := exec.LookPath("podman") if err != nil { return err diff --git a/core/dpkg-buildpackage.go b/core/dpkg-buildpackage.go index f080271..c79abfd 100644 --- a/core/dpkg-buildpackage.go +++ b/core/dpkg-buildpackage.go @@ -13,7 +13,8 @@ type DpkgBuildModule struct { // BuildDpkgModule builds a module that builds a dpkg project // and installs the resulting .deb package -func BuildDpkgBuildPkgModule(module DpkgBuildModule) (string, error) { +func BuildDpkgBuildPkgModule(moduleInterface interface{}, _ *api.Recipe) (string, error) { + module := moduleInterface.(DpkgBuildModule) cmd := fmt.Sprintf( "cd /sources/%s && dpkg-buildpackage -d -us -uc -b", module.Name, diff --git a/core/dpkg.go b/core/dpkg.go index a75d85c..81c2bd6 100644 --- a/core/dpkg.go +++ b/core/dpkg.go @@ -12,7 +12,8 @@ type DpkgModule struct { } // BuildDpkgModule builds a module that installs a .deb package -func BuildDpkgModule(module DpkgModule) (string, error) { +func BuildDpkgModule(moduleInterface interface{}, _ *api.Recipe) (string, error) { + module := moduleInterface.(DpkgModule) cmd := "" for _, path := range module.Source.Paths { cmd += fmt.Sprintf(" dpkg -i /sources/%s && apt install -f && ", path) diff --git a/core/go.go b/core/go.go index 3436da1..23336eb 100644 --- a/core/go.go +++ b/core/go.go @@ -1,10 +1,14 @@ package core -import "fmt" +import ( + "fmt" + "github.com/vanilla-os/vib/api" +) type GoModule struct { Name string `json:"name"` Type string `json:"type"` + Source api.Source BuildVars map[string]string BuildFlags string } @@ -12,7 +16,18 @@ type GoModule struct { // BuildGoModule builds a module that builds a Go project // buildVars are used to customize the build command // like setting the output binary name and location -func BuildGoModule(module GoModule) (string, error) { +func BuildGoModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { + module := moduleInterface.(GoModule) + + err := api.DownloadSource(recipe.DownloadsPath, module.Source) + if err != nil { + return "", err + } + err = api.MoveSource(recipe.DownloadsPath, module.Source) + if err != nil { + return "", err + } + buildVars := map[string]string{} for k, v := range module.BuildVars { buildVars[k] = v diff --git a/core/loader.go b/core/loader.go index 3d9499f..dcabd94 100644 --- a/core/loader.go +++ b/core/loader.go @@ -2,6 +2,9 @@ package core import ( "errors" + "fmt" + "github.com/mitchellh/mapstructure" + "github.com/vanilla-os/vib/api" "io" "os" "path/filepath" @@ -12,8 +15,8 @@ import ( // LoadRecipe loads a recipe from a file and returns a Recipe // Does not validate the recipe but it will catch some errors // a proper validation will be done in the future -func LoadRecipe(path string) (*Recipe, error) { - recipe := &Recipe{} +func LoadRecipe(path string) (*api.Recipe, error) { + recipe := &api.Recipe{} // we use the absolute path to the recipe file as the // root path for the recipe and all its files @@ -107,15 +110,22 @@ func LoadRecipe(path string) (*Recipe, error) { } // here we expand modules of type "includes" - var newRecipeModules map[string]interface{} + var newRecipeModules []interface{} - // cannot use the value from the second for loop here - // as it always gets reset and the first for loop would not reflect the amount properly - lenModules := 0 - for _, module := range recipe.Modules { + for _, moduleInterface := range recipe.Modules { - if module.(map[string]interface{})["Type"] == "includes" { - include := module.(IncludesModule) + var module Module + err := mapstructure.Decode(moduleInterface, &module) + if err != nil { + return nil, err + } + + if module.Type == "includes" { + var include IncludesModule + err := mapstructure.Decode(moduleInterface, &include) + if err != nil { + return nil, err + } if len(include.Includes) == 0 { return nil, errors.New("includes module must have at least one module to include") @@ -123,19 +133,18 @@ func LoadRecipe(path string) (*Recipe, error) { for _, include := range include.Includes { includeModule, err := GenModule(filepath.Join(recipe.ParentPath, include+".yml")) + fmt.Printf("!!!!adding new module %s\n", includeModule) if err != nil { return nil, err } - newRecipeModules[string(rune(lenModules))] = includeModule - lenModules += 1 + newRecipeModules = append(newRecipeModules, includeModule) } continue } - newRecipeModules[string(rune(lenModules))] = module.(map[string]interface{}) - lenModules += 1 + newRecipeModules = append(newRecipeModules, moduleInterface) } recipe.Modules = newRecipeModules diff --git a/core/make.go b/core/make.go index c0adb11..c121483 100644 --- a/core/make.go +++ b/core/make.go @@ -1,6 +1,25 @@ package core +import "github.com/vanilla-os/vib/api" + +type MakeModule struct { + Name string `json:"name"` + Type string `json:"type"` + Source api.Source +} + // BuildMakeModule builds a module that builds a Make project -func BuildMakeModule(module Module) (string, error) { +func BuildMakeModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { + module := moduleInterface.(MakeModule) + + err := api.DownloadSource(recipe.DownloadsPath, module.Source) + if err != nil { + return "", err + } + err = api.MoveSource(recipe.DownloadsPath, module.Source) + if err != nil { + return "", err + } + return "cd /sources/" + module.Name + " && make && make install", nil } diff --git a/core/meson.go b/core/meson.go index d46951e..77b2e10 100644 --- a/core/meson.go +++ b/core/meson.go @@ -2,14 +2,31 @@ package core import ( "fmt" + "github.com/vanilla-os/vib/api" "github.com/google/uuid" ) +type MesonModule struct { + Name string + Type string + Source api.Source +} + // BuildMesonModule builds a module that builds a Meson project -func BuildMesonModule(module Module) (string, error) { +func BuildMesonModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { + module := moduleInterface.(MesonModule) tmpDir := "/tmp/" + uuid.New().String() + err := api.DownloadSource(recipe.DownloadsPath, module.Source) + if err != nil { + return "", err + } + err = api.MoveSource(recipe.DownloadsPath, module.Source) + if err != nil { + return "", err + } + cmd := fmt.Sprintf( "cd /sources/%s && meson %s && ninja -C %s && ninja -C %s install", module.Name, diff --git a/core/plugins.go b/core/plugins.go index c956423..4d7727b 100644 --- a/core/plugins.go +++ b/core/plugins.go @@ -2,12 +2,13 @@ package core import ( "fmt" + "github.com/vanilla-os/vib/api" "plugin" ) var openedPlugins map[string]Plugin -func LoadPlugin(name string, module interface{}) (string, error) { +func LoadPlugin(name string, module interface{}, recipe *api.Recipe) (string, error) { pluginOpened := false var buildModule Plugin buildModule, pluginOpened = openedPlugins[name] @@ -23,12 +24,12 @@ func LoadPlugin(name string, module interface{}) (string, error) { if err != nil { panic(err) } - buildModule.BuildFunc = buildFunction.(func(interface{}) (string, error)) + buildModule.BuildFunc = buildFunction.(func(interface{}, *api.Recipe) (string, error)) buildModule.LoadedPlugin = loadedPlugin openedPlugins[name] = buildModule } fmt.Printf("Using plugin: %s\n", buildModule.Name) - fmt.Println(buildModule.BuildFunc(module)) - return buildModule.BuildFunc(module) + fmt.Println(buildModule.BuildFunc(module, recipe)) + return buildModule.BuildFunc(module, recipe) } diff --git a/core/shell.go b/core/shell.go index 10c9ce1..3f78505 100644 --- a/core/shell.go +++ b/core/shell.go @@ -1,6 +1,11 @@ package core -import "errors" +import ( + "errors" + "fmt" + "github.com/mitchellh/mapstructure" + "github.com/vanilla-os/vib/api" +) type ShellModule struct { Name string `json:"name"` @@ -8,7 +13,12 @@ type ShellModule struct { Commands []string } -func BuildShellModule(module ShellModule) (string, error) { +func BuildShellModule(moduleInterface interface{}, _ *api.Recipe) (string, error) { + var module ShellModule + mapstructure.Decode(moduleInterface, &module) + fmt.Println(moduleInterface) + fmt.Println(module) + fmt.Println(module.Commands) if len(module.Commands) == 0 { return "", errors.New("no commands specified") } diff --git a/core/structs.go b/core/structs.go index 461a150..b744d58 100644 --- a/core/structs.go +++ b/core/structs.go @@ -1,27 +1,10 @@ package core import ( + "github.com/vanilla-os/vib/api" "plugin" ) -type Recipe struct { - Base string `json:"base"` - Name string - Id string - SingleLayer bool `json:"singlelayer"` - Labels map[string]string `json:"labels"` - Adds map[string]string `json:"adds"` - Args map[string]string `json:"args"` - Runs []string `json:"runs"` - Cmd string `json:"cmd"` - Modules map[string]interface{} `json:"modules"` - Path string - ParentPath string - DownloadsPath string - SourcesPath string - Containerfile string -} - type Module struct { Name string `json:"name"` Type string `json:"type"` @@ -41,6 +24,6 @@ type ModuleCommand struct { type Plugin struct { Name string - BuildFunc func(interface{}) (string, error) + BuildFunc func(interface{}, *api.Recipe) (string, error) LoadedPlugin *plugin.Plugin } diff --git a/go.mod b/go.mod index 21d7a62..a23854c 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,8 @@ go 1.20 require github.com/spf13/cobra v1.7.0 +require github.com/mitchellh/mapstructure v1.5.0 // indirect + require ( github.com/google/uuid v1.3.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 838df8c..ff2322b 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +3,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=