From eeac0f7dbff0d1c50dd0acfa413fd5fbb4aae10f Mon Sep 17 00:00:00 2001 From: axtloss Date: Wed, 15 Nov 2023 21:48:23 +0100 Subject: [PATCH 01/14] feat: add logic for plugin loading Adds logic for loading and reusing module plugins --- core/plugins.go | 41 +++++++++++++++++++++++++++++++++++++++++ core/structs.go | 8 ++++++++ 2 files changed, 49 insertions(+) create mode 100644 core/plugins.go diff --git a/core/plugins.go b/core/plugins.go new file mode 100644 index 0000000..12ff772 --- /dev/null +++ b/core/plugins.go @@ -0,0 +1,41 @@ +package core + +import ( + "fmt" + "plugin" + "strings" +) + +var openedPlugins []Plugin + +func LoadPlugin(name string, module []byte) { + pluginOpened := false + var buildModule Plugin + for _, plugin := range openedPlugins { + // To avoid loading the same plugin multiple times, we check the openedPlugins variable to see if the plugin + // was loaded and added to the variable before + if strings.ToLower(plugin.Name) == strings.ToLower(name) { + pluginOpened = true + buildModule = plugin + } + } + if !pluginOpened { + fmt.Println("Loading new plugin") + buildModule = Plugin{Name: name} + var err error + loadedPlugin, err := plugin.Open(fmt.Sprintf("./plugins/%s.so", name)) // TODO: Proper path resolving + if err != nil { + panic(err) + } + buildFunction, err := loadedPlugin.Lookup("BuildModule") + if err != nil { + panic(err) + } + buildModule.BuildFunc = buildFunction.(func([]byte) (string, error)) + buildModule.LoadedPlugin = loadedPlugin + + openedPlugins = append(openedPlugins, buildModule) + } + fmt.Printf("Using plugin: %s\n", buildModule.Name) + fmt.Println(buildModule.BuildFunc([]byte(":3"))) +} diff --git a/core/structs.go b/core/structs.go index 23febcb..898437a 100644 --- a/core/structs.go +++ b/core/structs.go @@ -1,5 +1,7 @@ package core +import "plugin" + type Recipe struct { Base string `json:"base"` Name string @@ -46,3 +48,9 @@ type ModuleCommand struct { Name string Command string } + +type Plugin struct { + Name string + BuildFunc func([]byte) (string, error) + LoadedPlugin *plugin.Plugin +} From f98a719dc394249c724b4d4beac2dfe3a84a5e87 Mon Sep 17 00:00:00 2001 From: axtloss Date: Tue, 21 Nov 2023 18:38:58 +0100 Subject: [PATCH 02/14] Rework recipe loading to allow custom Module structs Reworks the way recipes are loaded to allow Module plugins to make use of their own Module definitions instead of having to adjust to a central one. --- api/structs.go | 13 ++++++++++ core/apt.go | 9 ++++++- core/build.go | 46 +++++++++++++++++------------------ core/cmake.go | 9 ++++++- core/dpkg-buildpackage.go | 13 ++++++++-- core/dpkg.go | 13 ++++++++-- core/go.go | 9 ++++++- core/loader.go | 50 +++++++++++++++------------------------ core/plugins.go | 21 ++++++---------- core/resolver.go | 7 +++--- core/shell.go | 8 ++++++- core/structs.go | 44 +++++++++++++--------------------- 12 files changed, 136 insertions(+), 106 deletions(-) create mode 100644 api/structs.go diff --git a/api/structs.go b/api/structs.go new file mode 100644 index 0000000..d1adce7 --- /dev/null +++ b/api/structs.go @@ -0,0 +1,13 @@ +package api + +type Source struct { + URL string `json:"url"` + Checksum string `json:"checksum"` + Type string `json:"type"` + Commit string `json:"commit"` + Tag string `json:"tag"` + Branch string `json:"branch"` + Packages []string `json:"packages"` + Paths []string `json:"paths"` + Module string +} diff --git a/core/apt.go b/core/apt.go index 72c374a..c5d9bac 100644 --- a/core/apt.go +++ b/core/apt.go @@ -4,13 +4,20 @@ import ( "bufio" "errors" "fmt" + "github.com/vanilla-os/vib/api" "os" "path/filepath" ) +type AptModule struct { + Name string `json:"name"` + Type string `json:"name"` + Source api.Source `json:"source"` +} + // BuildAptModule builds a module that installs packages // using the apt package manager -func BuildAptModule(recipe *Recipe, module Module) (string, error) { +func BuildAptModule(recipe *Recipe, module AptModule) (string, error) { if len(module.Source.Packages) > 0 { packages := "" for _, pkg := range module.Source.Packages { diff --git a/core/build.go b/core/build.go index ae68c79..5e2f1f7 100644 --- a/core/build.go +++ b/core/build.go @@ -16,22 +16,22 @@ func BuildRecipe(recipePath string) (Recipe, error) { fmt.Printf("Building recipe %s\n", recipe.Name) // resolve (and download) the sources - modules, sources, err := ResolveSources(recipe) - if err != nil { - return Recipe{}, err - } + //modules, sources, err := ResolveSources(recipe) + //if err != nil { + // return Recipe{}, err + //} // move them to the sources directory so they can be // used by the modules during the build - err = MoveSources(recipe, sources) - if err != nil { - return Recipe{}, err - } + //err = MoveSources(recipe, sources) + //if err != nil { + // return Recipe{}, err + //} // build the modules* // * actually just build the commands that will be used // in the Containerfile to build the modules - cmds, err := BuildModules(recipe, modules) + cmds, err := BuildModules(recipe, recipe.Modules) if err != nil { return Recipe{}, err } @@ -172,11 +172,11 @@ func BuildContainerfile(recipe *Recipe, cmds []ModuleCommand) error { } // BuildModules builds a list of modules commands from a list of modules -func BuildModules(recipe *Recipe, modules []Module) ([]ModuleCommand, error) { +func BuildModules(recipe *Recipe, modules map[string]interface{}) ([]ModuleCommand, error) { cmds := []ModuleCommand{} for _, module := range modules { - fmt.Printf("Creating build command for %s\n", module.Name) + fmt.Printf("Creating build command for %s\n", module.(Module).Name) cmd, err := BuildModule(recipe, module) if err != nil { @@ -184,7 +184,7 @@ func BuildModules(recipe *Recipe, modules []Module) ([]ModuleCommand, error) { } cmds = append(cmds, ModuleCommand{ - Name: module.Name, + Name: module.(Module).Name, Command: cmd, }) } @@ -195,25 +195,25 @@ func BuildModules(recipe *Recipe, modules []Module) ([]ModuleCommand, error) { // 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 Module) (string, error) { - switch module.Type { +func BuildModule(recipe *Recipe, module interface{}) (string, error) { + switch module.(Module).Type { case "apt": - return BuildAptModule(recipe, module) + return BuildAptModule(recipe, module.(AptModule)) case "cmake": - return BuildCMakeModule(module) + return BuildCMakeModule(module.(CMakeModule)) case "dpkg": - return BuildDpkgModule(module) + return BuildDpkgModule(module.(DpkgModule)) case "dpkg-buildpackage": - return BuildDpkgBuildPkgModule(module) + return BuildDpkgBuildPkgModule(module.(DpkgBuildModule)) case "go": - return BuildGoModule(module) + return BuildGoModule(module.(GoModule)) case "make": - return BuildMakeModule(module) + return BuildMakeModule(module.(Module)) case "meson": - return BuildMesonModule(module) + return BuildMesonModule(module.(Module)) case "shell": - return BuildShellModule(module) + return BuildShellModule(module.(ShellModule)) default: - return "", fmt.Errorf("unknown module type %s", module.Type) + return LoadPlugin(module.(Module).Type, module) } } diff --git a/core/cmake.go b/core/cmake.go index 384352e..f0cb1b0 100644 --- a/core/cmake.go +++ b/core/cmake.go @@ -2,8 +2,15 @@ package core import "fmt" +type CMakeModule struct { + Name string `json:"name"` + Type string `json:"name"` + BuildVars map[string]string `json:"name"` + BuildFlags string `json:"name"` +} + // BuildCMakeModule builds a module that builds a CMake project -func BuildCMakeModule(module Module) (string, error) { +func BuildCMakeModule(module CMakeModule) (string, error) { buildVars := map[string]string{} for k, v := range module.BuildVars { buildVars[k] = v diff --git a/core/dpkg-buildpackage.go b/core/dpkg-buildpackage.go index dbe2dae..f080271 100644 --- a/core/dpkg-buildpackage.go +++ b/core/dpkg-buildpackage.go @@ -1,10 +1,19 @@ package core -import "fmt" +import ( + "fmt" + "github.com/vanilla-os/vib/api" +) + +type DpkgBuildModule struct { + Name string `json:"name"` + Type string `json:"type"` + Source api.Source +} // BuildDpkgModule builds a module that builds a dpkg project // and installs the resulting .deb package -func BuildDpkgBuildPkgModule(module Module) (string, error) { +func BuildDpkgBuildPkgModule(module DpkgBuildModule) (string, error) { 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 97a5f13..a75d85c 100644 --- a/core/dpkg.go +++ b/core/dpkg.go @@ -1,9 +1,18 @@ package core -import "fmt" +import ( + "fmt" + "github.com/vanilla-os/vib/api" +) + +type DpkgModule struct { + Name string `json:"name"` + Type string `json:"string"` + Source api.Source +} // BuildDpkgModule builds a module that installs a .deb package -func BuildDpkgModule(module Module) (string, error) { +func BuildDpkgModule(module DpkgModule) (string, error) { 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 e674d12..3436da1 100644 --- a/core/go.go +++ b/core/go.go @@ -2,10 +2,17 @@ package core import "fmt" +type GoModule struct { + Name string `json:"name"` + Type string `json:"type"` + BuildVars map[string]string + BuildFlags string +} + // 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 Module) (string, error) { +func BuildGoModule(module GoModule) (string, error) { buildVars := map[string]string{} for k, v := range module.BuildVars { buildVars[k] = v diff --git a/core/loader.go b/core/loader.go index 8231aa7..3d9499f 100644 --- a/core/loader.go +++ b/core/loader.go @@ -106,48 +106,36 @@ func LoadRecipe(path string) (*Recipe, error) { } } - // here we expand modules of type "gen-modules" - newRecipeModules := []Module{} + // here we expand modules of type "includes" + var newRecipeModules map[string]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 { - if module.Type == "gen-modules" { // DEPRECATED: use the "includes" module instead - genModulePaths, err := filepath.Glob(filepath.Join(recipe.ParentPath, module.Path, "*.yml")) - if err != nil { - return nil, err - } - - genModules := []Module{} - for _, genModulePath := range genModulePaths { - genModule, err := GenModule(genModulePath) + if module.(map[string]interface{})["Type"] == "includes" { + include := module.(IncludesModule) - if err != nil { - return nil, err - } - - genModules = append(genModules, genModule) - } - - newRecipeModules = append(newRecipeModules, genModules...) - continue - } else if module.Type == "includes" { - if len(module.Includes) == 0 { + if len(include.Includes) == 0 { return nil, errors.New("includes module must have at least one module to include") } - for _, include := range module.Includes { + for _, include := range include.Includes { includeModule, err := GenModule(filepath.Join(recipe.ParentPath, include+".yml")) if err != nil { return nil, err } - newRecipeModules = append(newRecipeModules, includeModule) + newRecipeModules[string(rune(lenModules))] = includeModule + lenModules += 1 } continue } - newRecipeModules = append(newRecipeModules, module) + newRecipeModules[string(rune(lenModules))] = module.(map[string]interface{}) + lenModules += 1 } recipe.Modules = newRecipeModules @@ -156,24 +144,24 @@ func LoadRecipe(path string) (*Recipe, error) { } // GenModule generate a Module struct from a module path -func GenModule(modulePath string) (Module, error) { - module := &Module{} +func GenModule(modulePath string) (map[string]interface{}, error) { + var module map[string]interface{} moduleFile, err := os.Open(modulePath) if err != nil { - return *module, err + return module, err } defer moduleFile.Close() moduleYAML, err := io.ReadAll(moduleFile) if err != nil { - return *module, err + return module, err } err = yaml.Unmarshal(moduleYAML, module) if err != nil { - return *module, err + return module, err } - return *module, nil + return module, nil } diff --git a/core/plugins.go b/core/plugins.go index 12ff772..c956423 100644 --- a/core/plugins.go +++ b/core/plugins.go @@ -3,22 +3,14 @@ package core import ( "fmt" "plugin" - "strings" ) -var openedPlugins []Plugin +var openedPlugins map[string]Plugin -func LoadPlugin(name string, module []byte) { +func LoadPlugin(name string, module interface{}) (string, error) { pluginOpened := false var buildModule Plugin - for _, plugin := range openedPlugins { - // To avoid loading the same plugin multiple times, we check the openedPlugins variable to see if the plugin - // was loaded and added to the variable before - if strings.ToLower(plugin.Name) == strings.ToLower(name) { - pluginOpened = true - buildModule = plugin - } - } + buildModule, pluginOpened = openedPlugins[name] if !pluginOpened { fmt.Println("Loading new plugin") buildModule = Plugin{Name: name} @@ -31,11 +23,12 @@ func LoadPlugin(name string, module []byte) { if err != nil { panic(err) } - buildModule.BuildFunc = buildFunction.(func([]byte) (string, error)) + buildModule.BuildFunc = buildFunction.(func(interface{}) (string, error)) buildModule.LoadedPlugin = loadedPlugin - openedPlugins = append(openedPlugins, buildModule) + openedPlugins[name] = buildModule } fmt.Printf("Using plugin: %s\n", buildModule.Name) - fmt.Println(buildModule.BuildFunc([]byte(":3"))) + fmt.Println(buildModule.BuildFunc(module)) + return buildModule.BuildFunc(module) } diff --git a/core/resolver.go b/core/resolver.go index 52e0d76..bb8f828 100644 --- a/core/resolver.go +++ b/core/resolver.go @@ -3,6 +3,7 @@ package core import ( "crypto/sha256" "fmt" + "github.com/vanilla-os/vib/api" "io" "net/http" "os" @@ -14,11 +15,11 @@ import ( // 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, []Source, error) { +func ResolveSources(recipe *Recipe) ([]Module, []api.Source, error) { fmt.Println("Resolving sources") modules := GetAllModules(recipe.Modules) - var sources []Source + //var sources []Source for _, module := range modules { fmt.Printf("Resolving source for: %s\n", module.Name) @@ -39,7 +40,7 @@ func ResolveSources(recipe *Recipe) ([]Module, []Source, error) { return modules, sources, nil } -// GetAllModules returns a list of all modules in a ordered list +// GetAllModules returns a list of all modules in an ordered list func GetAllModules(modules []Module) []Module { var orderedList []Module diff --git a/core/shell.go b/core/shell.go index 4d06508..10c9ce1 100644 --- a/core/shell.go +++ b/core/shell.go @@ -2,7 +2,13 @@ package core import "errors" -func BuildShellModule(module Module) (string, error) { +type ShellModule struct { + Name string `json:"name"` + Type string `json:"type"` + Commands []string +} + +func BuildShellModule(module ShellModule) (string, error) { if len(module.Commands) == 0 { return "", errors.New("no commands specified") } diff --git a/core/structs.go b/core/structs.go index 898437a..461a150 100644 --- a/core/structs.go +++ b/core/structs.go @@ -1,18 +1,20 @@ package core -import "plugin" +import ( + "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 []Module `json:"modules"` + 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 @@ -21,27 +23,15 @@ type Recipe struct { } type Module struct { - Name string `json:"name"` - Type string `json:"type"` - Path string `json:"path"` - Source Source `json:"source"` - Modules []Module `json:"modules"` - BuildFlags string `json:"buildflags"` - BuildVars map[string]string `json:"buildvars"` - Commands []string `json:"commands"` - Includes []string `json:"includes"` + Name string `json:"name"` + Type string `json:"type"` + Content []byte // The entire module unparsed as a []byte, used by plugins } -type Source struct { - URL string `json:"url"` - Checksum string `json:"checksum"` +type IncludesModule struct { + Name string `json:"name"` Type string `json:"type"` - Commit string `json:"commit"` - Tag string `json:"tag"` - Branch string `json:"branch"` - Packages []string `json:"packages"` - Paths []string `json:"paths"` - Module string + Includes []string `json:"includes"` } type ModuleCommand struct { @@ -51,6 +41,6 @@ type ModuleCommand struct { type Plugin struct { Name string - BuildFunc func([]byte) (string, error) + BuildFunc func(interface{}) (string, error) LoadedPlugin *plugin.Plugin } From 6eea1f61f89c7d728a4a154201c85b5783ce6db5 Mon Sep 17 00:00:00 2001 From: axtloss Date: Tue, 28 Nov 2023 20:07:43 +0100 Subject: [PATCH 03/14] 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= From dc13a62884035752f8f7bf02e3e56903355fc963 Mon Sep 17 00:00:00 2001 From: axtloss Date: Tue, 28 Nov 2023 21:41:01 +0100 Subject: [PATCH 04/14] Complete basic plugin support vib can now build basic recepies that do not contain nested modules! --- api/download.go | 34 +++++++++++++++++----------------- api/structs.go | 1 - core/apt.go | 7 ++++++- core/cmake.go | 11 ++++++++--- core/dpkg-buildpackage.go | 19 +++++++++++++++++-- core/dpkg.go | 17 +++++++++++++++-- core/go.go | 12 ++++++++---- core/loader.go | 9 ++++++++- core/make.go | 16 +++++++++++----- core/meson.go | 12 ++++++++---- core/shell.go | 25 +++++++++++++++++++------ core/structs.go | 1 + 12 files changed, 118 insertions(+), 46 deletions(-) diff --git a/api/download.go b/api/download.go index f7c86e0..19792b6 100644 --- a/api/download.go +++ b/api/download.go @@ -13,17 +13,17 @@ import ( // DownloadSource downloads a source to the downloads directory // according to its type (git, tar, ...) -func DownloadSource(downloadPath string, source Source) error { +func DownloadSource(downloadPath string, source Source, moduleName string) error { fmt.Printf("Downloading source: %s\n", source.URL) if source.Type == "git" { - return DownloadGitSource(downloadPath, source) + return DownloadGitSource(downloadPath, source, moduleName) } else if source.Type == "tar" { - err := DownloadTarSource(downloadPath, source) + err := DownloadTarSource(downloadPath, source, moduleName) if err != nil { return err } - return checksumValidation(source, filepath.Join(downloadPath, source.Module)) + return checksumValidation(source, filepath.Join(downloadPath, moduleName)) } else { return fmt.Errorf("unsupported source type %s", source.Type) } @@ -31,10 +31,10 @@ func DownloadSource(downloadPath string, source Source) error { // DownloadGitSource downloads a git source to the downloads directory // and checks out the commit or tag -func DownloadGitSource(downloadPath string, source Source) error { +func DownloadGitSource(downloadPath string, source Source, moduleName string) error { fmt.Printf("Source is git: %s\n", source.URL) - dest := filepath.Join(downloadPath, source.Module) + dest := filepath.Join(downloadPath, moduleName) if source.Commit == "" && source.Tag == "" && source.Branch == "" { return fmt.Errorf("missing source commit, tag or branch") @@ -116,10 +116,10 @@ func DownloadGitSource(downloadPath string, source Source) error { } // DownloadTarSource downloads a tar archive to the downloads directory -func DownloadTarSource(downloadPath string, source Source) error { +func DownloadTarSource(downloadPath string, source Source, moduleName string) error { fmt.Printf("Source is tar: %s\n", source.URL) //Create the destination path - dest := filepath.Join(downloadPath, source.Module) + dest := filepath.Join(downloadPath, moduleName) //Download the resource res, err := http.Get(source.URL) if err != nil { @@ -145,11 +145,11 @@ func DownloadTarSource(downloadPath string, source Source) error { // MoveSources moves all sources from the downloads directory to the // sources directory -func MoveSources(downloadPath string, sources []Source) error { +func MoveSources(downloadPath string, sourcesPath string, sources []Source, moduleName string) error { fmt.Println("Moving sources") for _, source := range sources { - err := MoveSource(downloadPath, source) + err := MoveSource(downloadPath, sourcesPath, source, moduleName) if err != nil { return err } @@ -161,26 +161,26 @@ func MoveSources(downloadPath string, 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(downloadPath string, source Source) error { - fmt.Printf("Moving source: %s\n", source.Module) +func MoveSource(downloadPath string, sourcesPath string, source Source, moduleName string) error { + fmt.Printf("Moving source: %s\n", moduleName) if source.Type == "git" { return os.Rename( - filepath.Join(downloadPath, source.Module), - filepath.Join(downloadPath, source.Module), + filepath.Join(downloadPath, moduleName), + filepath.Join(sourcesPath, moduleName), ) } else if source.Type == "tar" { cmd := exec.Command( "tar", - "-xf", filepath.Join(downloadPath, source.Module), - "-C", downloadPath, + "-xf", filepath.Join(downloadPath, moduleName), + "-C", sourcesPath, ) err := cmd.Run() if err != nil { return err } - return os.Remove(filepath.Join(downloadPath, source.Module)) + return os.Remove(filepath.Join(downloadPath, moduleName)) } else { return fmt.Errorf("unsupported source type %s", source.Type) } diff --git a/api/structs.go b/api/structs.go index fb7540c..7ab5009 100644 --- a/api/structs.go +++ b/api/structs.go @@ -9,7 +9,6 @@ type Source struct { Branch string `json:"branch"` Packages []string `json:"packages"` Paths []string `json:"paths"` - Module string } type Recipe struct { diff --git a/core/apt.go b/core/apt.go index 5b861ff..96692ef 100644 --- a/core/apt.go +++ b/core/apt.go @@ -4,6 +4,7 @@ import ( "bufio" "errors" "fmt" + "github.com/mitchellh/mapstructure" "github.com/vanilla-os/vib/api" "os" "path/filepath" @@ -18,7 +19,11 @@ type AptModule struct { // BuildAptModule builds a module that installs packages // using the apt package manager func BuildAptModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { - module := moduleInterface.(AptModule) + var module AptModule + err := mapstructure.Decode(moduleInterface, &module) + if err != nil { + return "", err + } if len(module.Source.Packages) > 0 { packages := "" for _, pkg := range module.Source.Packages { diff --git a/core/cmake.go b/core/cmake.go index 2b1980d..8755e1a 100644 --- a/core/cmake.go +++ b/core/cmake.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "github.com/mitchellh/mapstructure" "github.com/vanilla-os/vib/api" ) @@ -16,12 +17,16 @@ type CMakeModule struct { // BuildCMakeModule builds a module that builds a CMake project func BuildCMakeModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { - module := moduleInterface.(CMakeModule) - err := api.DownloadSource(recipe.DownloadsPath, module.Source) + var module CMakeModule + err := mapstructure.Decode(moduleInterface, &module) if err != nil { return "", err } - err = api.MoveSource(recipe.DownloadsPath, module.Source) + err = api.DownloadSource(recipe.DownloadsPath, module.Source, module.Name) + if err != nil { + return "", err + } + err = api.MoveSource(recipe.DownloadsPath, recipe.SourcesPath, module.Source, module.Name) if err != nil { return "", err } diff --git a/core/dpkg-buildpackage.go b/core/dpkg-buildpackage.go index c79abfd..7d6d229 100644 --- a/core/dpkg-buildpackage.go +++ b/core/dpkg-buildpackage.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "github.com/mitchellh/mapstructure" "github.com/vanilla-os/vib/api" ) @@ -13,8 +14,22 @@ type DpkgBuildModule struct { // BuildDpkgModule builds a module that builds a dpkg project // and installs the resulting .deb package -func BuildDpkgBuildPkgModule(moduleInterface interface{}, _ *api.Recipe) (string, error) { - module := moduleInterface.(DpkgBuildModule) +func BuildDpkgBuildPkgModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { + var module DpkgBuildModule + err := mapstructure.Decode(moduleInterface, &module) + if err != nil { + return "", err + } + + err = api.DownloadSource(recipe.DownloadsPath, module.Source, module.Name) + if err != nil { + return "", err + } + err = api.MoveSource(recipe.DownloadsPath, recipe.SourcesPath, module.Source, module.Name) + if err != nil { + return "", err + } + 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 81c2bd6..52969d0 100644 --- a/core/dpkg.go +++ b/core/dpkg.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "github.com/mitchellh/mapstructure" "github.com/vanilla-os/vib/api" ) @@ -12,8 +13,20 @@ type DpkgModule struct { } // BuildDpkgModule builds a module that installs a .deb package -func BuildDpkgModule(moduleInterface interface{}, _ *api.Recipe) (string, error) { - module := moduleInterface.(DpkgModule) +func BuildDpkgModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { + var module CMakeModule + err := mapstructure.Decode(moduleInterface, &module) + if err != nil { + return "", err + } + err = api.DownloadSource(recipe.DownloadsPath, module.Source, module.Name) + if err != nil { + return "", err + } + err = api.MoveSource(recipe.DownloadsPath, recipe.SourcesPath, module.Source, module.Name) + if err != nil { + return "", err + } 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 23336eb..a9437d1 100644 --- a/core/go.go +++ b/core/go.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "github.com/mitchellh/mapstructure" "github.com/vanilla-os/vib/api" ) @@ -17,13 +18,16 @@ type GoModule struct { // buildVars are used to customize the build command // like setting the output binary name and location func BuildGoModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { - module := moduleInterface.(GoModule) - - err := api.DownloadSource(recipe.DownloadsPath, module.Source) + var module GoModule + err := mapstructure.Decode(moduleInterface, &module) + if err != nil { + return "", err + } + err = api.DownloadSource(recipe.DownloadsPath, module.Source, module.Name) if err != nil { return "", err } - err = api.MoveSource(recipe.DownloadsPath, module.Source) + err = api.MoveSource(recipe.DownloadsPath, recipe.SourcesPath, module.Source, module.Name) if err != nil { return "", err } diff --git a/core/loader.go b/core/loader.go index dcabd94..b14b741 100644 --- a/core/loader.go +++ b/core/loader.go @@ -167,10 +167,17 @@ func GenModule(modulePath string) (map[string]interface{}, error) { return module, err } - err = yaml.Unmarshal(moduleYAML, module) + fmt.Println("====") + fmt.Println(&module) + fmt.Println(string(moduleYAML)) + err = yaml.Unmarshal(moduleYAML, &module) if err != nil { return module, err } + fmt.Println() + fmt.Println("no error") + fmt.Println(module) + fmt.Println("=====") return module, nil } diff --git a/core/make.go b/core/make.go index c121483..15587d3 100644 --- a/core/make.go +++ b/core/make.go @@ -1,6 +1,9 @@ package core -import "github.com/vanilla-os/vib/api" +import ( + "github.com/mitchellh/mapstructure" + "github.com/vanilla-os/vib/api" +) type MakeModule struct { Name string `json:"name"` @@ -10,13 +13,16 @@ type MakeModule struct { // BuildMakeModule builds a module that builds a Make project func BuildMakeModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { - module := moduleInterface.(MakeModule) - - err := api.DownloadSource(recipe.DownloadsPath, module.Source) + var module MakeModule + err := mapstructure.Decode(moduleInterface, &module) + if err != nil { + return "", err + } + err = api.DownloadSource(recipe.DownloadsPath, module.Source, module.Name) if err != nil { return "", err } - err = api.MoveSource(recipe.DownloadsPath, module.Source) + err = api.MoveSource(recipe.DownloadsPath, recipe.SourcesPath, module.Source, module.Name) if err != nil { return "", err } diff --git a/core/meson.go b/core/meson.go index 77b2e10..0d5486e 100644 --- a/core/meson.go +++ b/core/meson.go @@ -2,6 +2,7 @@ package core import ( "fmt" + "github.com/mitchellh/mapstructure" "github.com/vanilla-os/vib/api" "github.com/google/uuid" @@ -15,14 +16,17 @@ type MesonModule struct { // BuildMesonModule builds a module that builds a Meson project func BuildMesonModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { - module := moduleInterface.(MesonModule) tmpDir := "/tmp/" + uuid.New().String() - - err := api.DownloadSource(recipe.DownloadsPath, module.Source) + var module MesonModule + err := mapstructure.Decode(moduleInterface, &module) + if err != nil { + return "", err + } + err = api.DownloadSource(recipe.DownloadsPath, module.Source, module.Name) if err != nil { return "", err } - err = api.MoveSource(recipe.DownloadsPath, module.Source) + err = api.MoveSource(recipe.DownloadsPath, recipe.SourcesPath, module.Source, module.Name) if err != nil { return "", err } diff --git a/core/shell.go b/core/shell.go index 3f78505..b292072 100644 --- a/core/shell.go +++ b/core/shell.go @@ -2,23 +2,36 @@ package core import ( "errors" - "fmt" "github.com/mitchellh/mapstructure" "github.com/vanilla-os/vib/api" + "strings" ) type ShellModule struct { Name string `json:"name"` Type string `json:"type"` + Source api.Source Commands []string } -func BuildShellModule(moduleInterface interface{}, _ *api.Recipe) (string, error) { +func BuildShellModule(moduleInterface interface{}, recipe *api.Recipe) (string, error) { var module ShellModule - mapstructure.Decode(moduleInterface, &module) - fmt.Println(moduleInterface) - fmt.Println(module) - fmt.Println(module.Commands) + err := mapstructure.Decode(moduleInterface, &module) + if err != nil { + return "", err + } + + if strings.TrimSpace(module.Source.Type) != "" { + err := api.DownloadSource(recipe.DownloadsPath, module.Source, module.Name) + if err != nil { + return "", err + } + err = api.MoveSource(recipe.DownloadsPath, recipe.SourcesPath, module.Source, module.Name) + if err != nil { + return "", err + } + } + if len(module.Commands) == 0 { return "", errors.New("no commands specified") } diff --git a/core/structs.go b/core/structs.go index b744d58..78d0e46 100644 --- a/core/structs.go +++ b/core/structs.go @@ -8,6 +8,7 @@ import ( type Module struct { Name string `json:"name"` Type string `json:"type"` + Modules []map[string]interface{} Content []byte // The entire module unparsed as a []byte, used by plugins } From 1a4e0140466e8be6d81add62a904aa43eb94ce12 Mon Sep 17 00:00:00 2001 From: axtloss Date: Tue, 28 Nov 2023 22:14:23 +0100 Subject: [PATCH 05/14] feat: allow nested modules Allows users to nest modules, making them all run as one RUN command in the containerfile --- core/build.go | 73 ++++++++++++++++++++++++++++++++++++++++++-------- core/loader.go | 7 ----- 2 files changed, 62 insertions(+), 18 deletions(-) diff --git a/core/build.go b/core/build.go index afcec4c..6ae5821 100644 --- a/core/build.go +++ b/core/build.go @@ -5,6 +5,7 @@ import ( "github.com/mitchellh/mapstructure" "github.com/vanilla-os/vib/api" "os" + "strings" ) // BuildRecipe builds a Containerfile from a recipe path @@ -207,28 +208,78 @@ func BuildModule(recipe *api.Recipe, moduleInterface interface{}) (string, error if err != nil { return "", err } - fmt.Printf("Processing module: %s\n", module.Type) - fmt.Println(moduleInterface) + var commands string + if len(module.Modules) > 0 { + for _, nestedModule := range module.Modules { + fmt.Println("!!!!!!!!!!!!!!!!!!!!!!!nested module: ", module.Modules, "----", len(module.Modules)) + buildModule, err := BuildModule(recipe, nestedModule) + fmt.Println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", buildModule) + if err != nil { + return "", err + } + commands = buildModule + " && " + commands + } + } + + fmt.Println(commands) switch module.Type { case "apt": - return BuildAptModule(moduleInterface, recipe) + command, err := BuildAptModule(moduleInterface, recipe) + if err != nil { + return "", err + } + commands = command + " && " + commands case "cmake": - return BuildCMakeModule(moduleInterface, recipe) + command, err := BuildCMakeModule(moduleInterface, recipe) + if err != nil { + return "", err + } + commands = command + " && " + commands case "dpkg": - return BuildDpkgModule(moduleInterface, recipe) + command, err := BuildDpkgModule(moduleInterface, recipe) + if err != nil { + return "", err + } + commands = command + " && " + commands case "dpkg-buildpackage": - return BuildDpkgBuildPkgModule(moduleInterface, recipe) + command, err := BuildDpkgBuildPkgModule(moduleInterface, recipe) + if err != nil { + return "", err + } + commands = command + " && " + commands case "go": - return BuildGoModule(moduleInterface, recipe) + command, err := BuildGoModule(moduleInterface, recipe) + if err != nil { + return "", err + } + commands = command + " && " + commands case "make": - return BuildMakeModule(moduleInterface, recipe) + command, err := BuildMakeModule(moduleInterface, recipe) + if err != nil { + return "", err + } + commands = command + " && " + commands case "meson": - return BuildMesonModule(moduleInterface, recipe) + command, err := BuildMesonModule(moduleInterface, recipe) + if err != nil { + return "", err + } + commands = command + " && " + commands case "shell": - return BuildShellModule(moduleInterface, recipe) + command, err := BuildShellModule(moduleInterface, recipe) + if err != nil { + return "", err + } + commands = command + " && " + commands case "includes": return "", nil default: - return LoadPlugin(module.Type, moduleInterface, recipe) + command, err := LoadPlugin(module.Type, moduleInterface, recipe) + if err != nil { + return "", err + } + commands = command + " && " + commands } + + return strings.TrimSuffix(commands, " && "), err } diff --git a/core/loader.go b/core/loader.go index b14b741..d52f243 100644 --- a/core/loader.go +++ b/core/loader.go @@ -167,17 +167,10 @@ func GenModule(modulePath string) (map[string]interface{}, error) { return module, err } - fmt.Println("====") - fmt.Println(&module) - fmt.Println(string(moduleYAML)) err = yaml.Unmarshal(moduleYAML, &module) if err != nil { return module, err } - fmt.Println() - fmt.Println("no error") - fmt.Println(module) - fmt.Println("=====") return module, nil } From 464c5f0df8706cc3f79fdf75c4100820d6a3fd5c Mon Sep 17 00:00:00 2001 From: axtloss Date: Tue, 28 Nov 2023 22:16:06 +0100 Subject: [PATCH 06/14] feat: read plugin path from recipe Allows users to define plugin paths manually instead of having it hardcoded to ./plugins --- api/structs.go | 1 + core/plugins.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/api/structs.go b/api/structs.go index 7ab5009..7a6bb62 100644 --- a/api/structs.go +++ b/api/structs.go @@ -26,5 +26,6 @@ type Recipe struct { ParentPath string DownloadsPath string SourcesPath string + PluginPath string Containerfile string } diff --git a/core/plugins.go b/core/plugins.go index 4d7727b..b1f0df4 100644 --- a/core/plugins.go +++ b/core/plugins.go @@ -16,7 +16,7 @@ func LoadPlugin(name string, module interface{}, recipe *api.Recipe) (string, er fmt.Println("Loading new plugin") buildModule = Plugin{Name: name} var err error - loadedPlugin, err := plugin.Open(fmt.Sprintf("./plugins/%s.so", name)) // TODO: Proper path resolving + loadedPlugin, err := plugin.Open(fmt.Sprintf("%s/%s.so", recipe.PluginPath, name)) if err != nil { panic(err) } From bfcfbbabe4d9a3a42d14fb3be407972cf9eec382 Mon Sep 17 00:00:00 2001 From: axtloss Date: Tue, 28 Nov 2023 22:20:04 +0100 Subject: [PATCH 07/14] chore: remove stray debug prints --- core/build.go | 15 --------------- core/loader.go | 2 -- 2 files changed, 17 deletions(-) diff --git a/core/build.go b/core/build.go index 6ae5821..6378b95 100644 --- a/core/build.go +++ b/core/build.go @@ -18,19 +18,6 @@ func BuildRecipe(recipePath string) (api.Recipe, error) { fmt.Printf("Building recipe %s\n", recipe.Name) - // resolve (and download) the sources - //modules, sources, err := ResolveSources(recipe) - //if err != nil { - // return Recipe{}, err - //} - - // move them to the sources directory so they can be - // used by the modules during the build - //err = MoveSources(recipe, sources) - //if err != nil { - // return Recipe{}, err - //} - // build the modules* // * actually just build the commands that will be used // in the Containerfile to build the modules @@ -211,9 +198,7 @@ func BuildModule(recipe *api.Recipe, moduleInterface interface{}) (string, error var commands string if len(module.Modules) > 0 { for _, nestedModule := range module.Modules { - fmt.Println("!!!!!!!!!!!!!!!!!!!!!!!nested module: ", module.Modules, "----", len(module.Modules)) buildModule, err := BuildModule(recipe, nestedModule) - fmt.Println("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!", buildModule) if err != nil { return "", err } diff --git a/core/loader.go b/core/loader.go index d52f243..ae60eaa 100644 --- a/core/loader.go +++ b/core/loader.go @@ -2,7 +2,6 @@ package core import ( "errors" - "fmt" "github.com/mitchellh/mapstructure" "github.com/vanilla-os/vib/api" "io" @@ -133,7 +132,6 @@ func LoadRecipe(path string) (*api.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 } From 0747eebb68ac871dfb62e6579b4f6d9859ad65b0 Mon Sep 17 00:00:00 2001 From: axtloss Date: Tue, 28 Nov 2023 22:24:37 +0100 Subject: [PATCH 08/14] remove one last print --- core/build.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/core/build.go b/core/build.go index 6378b95..704dcec 100644 --- a/core/build.go +++ b/core/build.go @@ -170,7 +170,6 @@ func BuildModules(recipe *api.Recipe, modules []interface{}) ([]ModuleCommand, e if err != nil { return nil, err } - fmt.Printf("Creating build command for %s\n", module) cmd, err := BuildModule(recipe, moduleInterface) if err != nil { @@ -206,7 +205,6 @@ func BuildModule(recipe *api.Recipe, moduleInterface interface{}) (string, error } } - fmt.Println(commands) switch module.Type { case "apt": command, err := BuildAptModule(moduleInterface, recipe) From f7637a11ba642eb38b263fa45eedd5c07b13967b Mon Sep 17 00:00:00 2001 From: axtloss Date: Tue, 28 Nov 2023 22:31:08 +0100 Subject: [PATCH 09/14] fix: add nested modules in the proper orders Adds nested modules before the actual module as they are treated like dependencies and it does not make sense to have those after the module --- core/build.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/core/build.go b/core/build.go index 704dcec..888ab5e 100644 --- a/core/build.go +++ b/core/build.go @@ -201,7 +201,7 @@ func BuildModule(recipe *api.Recipe, moduleInterface interface{}) (string, error if err != nil { return "", err } - commands = buildModule + " && " + commands + commands = commands + " && " + buildModule } } @@ -211,49 +211,49 @@ func BuildModule(recipe *api.Recipe, moduleInterface interface{}) (string, error if err != nil { return "", err } - commands = command + " && " + commands + commands = commands + " && " + command case "cmake": command, err := BuildCMakeModule(moduleInterface, recipe) if err != nil { return "", err } - commands = command + " && " + commands + commands = commands + " && " + command case "dpkg": command, err := BuildDpkgModule(moduleInterface, recipe) if err != nil { return "", err } - commands = command + " && " + commands + commands = commands + " && " + command case "dpkg-buildpackage": command, err := BuildDpkgBuildPkgModule(moduleInterface, recipe) if err != nil { return "", err } - commands = command + " && " + commands + commands = commands + " && " + command case "go": command, err := BuildGoModule(moduleInterface, recipe) if err != nil { return "", err } - commands = command + " && " + commands + commands = commands + " && " + command case "make": command, err := BuildMakeModule(moduleInterface, recipe) if err != nil { return "", err } - commands = command + " && " + commands + commands = commands + " && " + command case "meson": command, err := BuildMesonModule(moduleInterface, recipe) if err != nil { return "", err } - commands = command + " && " + commands + commands = commands + " && " + command case "shell": command, err := BuildShellModule(moduleInterface, recipe) if err != nil { return "", err } - commands = command + " && " + commands + commands = commands + " && " + command case "includes": return "", nil default: @@ -261,8 +261,8 @@ func BuildModule(recipe *api.Recipe, moduleInterface interface{}) (string, error if err != nil { return "", err } - commands = command + " && " + commands + commands = commands + " && " + command } - return strings.TrimSuffix(commands, " && "), err + return strings.TrimPrefix(commands, " && "), err } From 225efd6ad9a9cd49e9c3d10fe6b62c19c1947be9 Mon Sep 17 00:00:00 2001 From: axtloss Date: Tue, 28 Nov 2023 22:52:42 +0100 Subject: [PATCH 10/14] define PluginPath in loader.go --- core/loader.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/loader.go b/core/loader.go index ae60eaa..4a6330e 100644 --- a/core/loader.go +++ b/core/loader.go @@ -83,6 +83,10 @@ func LoadRecipe(path string) (*api.Recipe, error) { return nil, err } + // the plugins directory contains all plugins that vib can load + // and use for unknown modules in the recipe + recipe.PluginPath = filepath.Join(filepath.Dir(recipePath), "plugins") + // the includes directory is the place where we store all the // files to be included in the container, this is useful for // example to include configuration files. Each file must follow From 1ca60917e5b6d61a65cb871ded3d63b82b779347 Mon Sep 17 00:00:00 2001 From: axtloss Date: Tue, 28 Nov 2023 23:20:30 +0100 Subject: [PATCH 11/14] fix: create map for plugins --- core/plugins.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/plugins.go b/core/plugins.go index b1f0df4..dafe7b8 100644 --- a/core/plugins.go +++ b/core/plugins.go @@ -9,6 +9,9 @@ import ( var openedPlugins map[string]Plugin func LoadPlugin(name string, module interface{}, recipe *api.Recipe) (string, error) { + if openedPlugins == nil { + openedPlugins = make(map[string]Plugin) + } pluginOpened := false var buildModule Plugin buildModule, pluginOpened = openedPlugins[name] From c572b3a17afef55ca65071c339769e2dcb93778b Mon Sep 17 00:00:00 2001 From: axtloss Date: Wed, 29 Nov 2023 19:08:03 +0100 Subject: [PATCH 12/14] move api to sepearte 'project' creates a seperate go.mod for api/ so that plugins and vib can build using the same version of the api, instead of having vib panic every time it loads a plugin because of differing versions --- api/go.mod | 3 +++ go.mod | 7 +++++-- go.sum | 1 + 3 files changed, 9 insertions(+), 2 deletions(-) create mode 100644 api/go.mod diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..9199c33 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,3 @@ +module github.com/vanilla-os/vib/api + +go 1.21.4 diff --git a/go.mod b/go.mod index a23854c..65d2aaf 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,17 @@ module github.com/vanilla-os/vib -go 1.20 +go 1.21.4 require github.com/spf13/cobra v1.7.0 -require github.com/mitchellh/mapstructure v1.5.0 // indirect +require github.com/mitchellh/mapstructure v1.5.0 require ( github.com/google/uuid v1.3.0 github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/vanilla-os/vib/api v0.0.0 gopkg.in/yaml.v3 v3.0.1 ) + +replace github.com/vanilla-os/vib/api v0.0.0 => ./api/ diff --git a/go.sum b/go.sum index ff2322b..f5b2fd5 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,7 @@ github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 91fd2d065331f038164862b2b2f1dd89235e1776 Mon Sep 17 00:00:00 2001 From: axtloss Date: Wed, 29 Nov 2023 19:10:32 +0100 Subject: [PATCH 13/14] fix: fix go.mod invalid version --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 65d2aaf..c84e767 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/vanilla-os/vib -go 1.21.4 +go 1.21 require github.com/spf13/cobra v1.7.0 From c843eaca2af68235946e641cf200ff24f52a8da1 Mon Sep 17 00:00:00 2001 From: axtloss Date: Sun, 3 Dec 2023 17:41:36 +0100 Subject: [PATCH 14/14] Change go version to 1.20 in api/go.mod --- api/go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/go.mod b/api/go.mod index 9199c33..ea4497b 100644 --- a/api/go.mod +++ b/api/go.mod @@ -1,3 +1,3 @@ module github.com/vanilla-os/vib/api -go 1.21.4 +go 1.21