From a909803b297cc192eab1d795a6c13dfbd9ffb3f7 Mon Sep 17 00:00:00 2001 From: Dan Anglin Date: Sat, 14 Sep 2024 05:58:43 +0100 Subject: [PATCH] refactor: reorganise magefiles Reorganise and refactor the magefiles to make it more manageable and reduce duplication. --- bash/profile.gotmpl | 1 + magefiles/{manage.go => all.go} | 8 +- magefiles/bash_profile.go | 18 ++- magefiles/directories.go | 15 +- magefiles/files.go | 81 ++++------- magefiles/{ => internal/config}/config.go | 50 +++---- magefiles/internal/templatefuncs/env.go | 7 + .../internal/templatefuncs/templatefuncs.go | 9 ++ .../utilities/utilities.go} | 54 +++++-- magefiles/internal/walk/copy_files.go | 64 +++++++++ magefiles/internal/walk/render_templates.go | 73 ++++++++++ magefiles/templates.go | 134 +++++------------- 12 files changed, 301 insertions(+), 213 deletions(-) rename magefiles/{manage.go => all.go} (51%) rename magefiles/{ => internal/config}/config.go (66%) create mode 100644 magefiles/internal/templatefuncs/env.go create mode 100644 magefiles/internal/templatefuncs/templatefuncs.go rename magefiles/{common.go => internal/utilities/utilities.go} (67%) create mode 100644 magefiles/internal/walk/copy_files.go create mode 100644 magefiles/internal/walk/render_templates.go diff --git a/bash/profile.gotmpl b/bash/profile.gotmpl index 3ccdad0..de12bee 100644 --- a/bash/profile.gotmpl +++ b/bash/profile.gotmpl @@ -56,3 +56,4 @@ PS2=" -> " ## {{ $command.Description }} {{ $command.Command }} {{- end -}} +{{ print "" }} diff --git a/magefiles/manage.go b/magefiles/all.go similarity index 51% rename from magefiles/manage.go rename to magefiles/all.go index 27ee376..db4e79e 100644 --- a/magefiles/manage.go +++ b/magefiles/all.go @@ -4,10 +4,12 @@ package main import "github.com/magefile/mage/mg" -var Default = Manage +const rootManagedDir string = "managed" -// Manage runs all the management tasks. -func Manage() error { +var Default = All + +// All runs all the management tasks. +func All() error { mg.Deps(Directories, Files, Templates) return nil diff --git a/magefiles/bash_profile.go b/magefiles/bash_profile.go index 35c0fd9..b574507 100644 --- a/magefiles/bash_profile.go +++ b/magefiles/bash_profile.go @@ -6,7 +6,9 @@ import ( "fmt" "os" "path/filepath" - "text/template" + + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/config" + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/utilities" ) // BashProfile manages the user's Bash Profile using their configuration and the Bash Profile template. @@ -22,31 +24,27 @@ func BashProfile() error { return fmt.Errorf("unable to get the user's home configuration directory: %w", err) } - config, err := newConfig() + cfg, err := config.NewConfig() if err != nil { return fmt.Errorf("unable to load the configuration: %w", err) } - if !config.BashProfile.Manage { + if !cfg.BashProfile.Manage { return nil } - funcMap := template.FuncMap{ - "env": env, - } - - if err := renderTemplate(config, bashProfileTemplateFile, managedBashProfile, funcMap); err != nil { + if err := utilities.RenderTemplate(cfg, bashProfileTemplateFile, managedBashProfile); err != nil { return fmt.Errorf("unable to generate the Bash Profile: %w", err) } - filename := config.BashProfile.Filename + filename := cfg.BashProfile.Filename if filename == "" { filename = defaultFilename } symlinkPath := filepath.Join(homeDirectory, filename) - if err := ensureSymlink(managedBashProfile, symlinkPath); err != nil { + if err := utilities.EnsureSymlink(managedBashProfile, symlinkPath); err != nil { return err } diff --git a/magefiles/directories.go b/magefiles/directories.go index 229e8e6..225938e 100644 --- a/magefiles/directories.go +++ b/magefiles/directories.go @@ -7,11 +7,14 @@ import ( "os" "path/filepath" "slices" + + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/config" + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/utilities" ) // Directories ensure that the specified home directories are present. func Directories() error { - config, err := newConfig() + cfg, err := config.NewConfig() if err != nil { return fmt.Errorf("unable to load the configuration: %w", err) } @@ -23,22 +26,22 @@ func Directories() error { directories := make([]string, 0) - if config.Directories.UseDefaultDirectories{ + if cfg.Directories.UseDefaultDirectories{ defaultHomeDirs := homeDirectories(userHome, defaultDirectories()) directories = append(directories, defaultHomeDirs...) } - if config.Directories.IncludeXDGDirectories{ + if cfg.Directories.IncludeXDGDirectories{ directories = append(directories, xdgDirectories()...) } - if len(config.Directories.AdditionalDirectories) != 0 { - additionalHomeDirs := homeDirectories(userHome, config.Directories.AdditionalDirectories) + if len(cfg.Directories.AdditionalDirectories) != 0 { + additionalHomeDirs := homeDirectories(userHome, cfg.Directories.AdditionalDirectories) directories = append(directories, additionalHomeDirs...) } for _, dir := range slices.All(directories) { - if err := ensureDirectory(dir); err != nil { + if err := utilities.EnsureDirectory(dir); err != nil { return fmt.Errorf("unable to ensure that %s is present: %w", dir, err) } diff --git a/magefiles/files.go b/magefiles/files.go index 26b61f2..9978742 100644 --- a/magefiles/files.go +++ b/magefiles/files.go @@ -4,81 +4,54 @@ package main import ( "fmt" - "io/fs" "os" "path/filepath" - "slices" "strings" - "github.com/magefile/mage/sh" + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/config" + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/utilities" + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/walk" ) // Files ensure that the configuration files in the managed directory is up to date and // ensures that they are symlinked correctly to the files in the user's home configuration // directory. func Files() error { - homeConfigDirectory, err := os.UserConfigDir() + const rootFilesDir string = "files" + + homeConfigDir, err := os.UserConfigDir() if err != nil { return fmt.Errorf("unable to get the user's home configuration directory: %w", err) } - config, err := newConfig() + cfg, err := config.NewConfig() if err != nil { return fmt.Errorf("unable to load the configuration: %w", err) } - managedConfig := managedConfigSet(config.ManagedConfigurations) + managedConfig := utilities.ManagedConfigSet(cfg.ManagedConfigurations) - if err = filepath.WalkDir(rootFilesDir, manageFilesFunc(homeConfigDirectory, managedConfig)); err != nil { - return fmt.Errorf("received an error while processing the files: %w", err) + validationFunc := func(relativePath string) bool { + split := strings.SplitN(relativePath, "/", 2) + + if len(split) < 1 { + return false + } + + appConfigName := split[0] + + _, exists := managedConfig[appConfigName] + + return exists + } + + if err = filepath.WalkDir( + rootFilesDir, + walk.CopyFiles(homeConfigDir, rootFilesDir, rootManagedDir, validationFunc), + ); err != nil { + return fmt.Errorf("received an error while copying the files: %w", err) } return nil } -func manageFilesFunc(homeConfigDirectory string, managedConfig map[string]struct{}) fs.WalkDirFunc { - return func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - if path == rootFilesDir { - return nil - } - - relativePath := strings.TrimPrefix(path, rootFilesDir+"/") - - appConfigName := strings.SplitN(relativePath, "/", 2)[0] - - if _, exists := managedConfig[appConfigName]; !exists { - return nil - } - - managedPath := filepath.Join(rootManagedDir, relativePath) - configPath := filepath.Join(homeConfigDirectory, relativePath) - - if d.IsDir() { - dirs := []string{managedPath, configPath} - - for _, dir := range slices.All(dirs) { - if err := ensureDirectory(dir); err != nil { - return fmt.Errorf("unable to ensure the existence of the directory %q: %w", dir, err) - } - } - - return nil - } - - fmt.Println("Processing file:", relativePath) - - if err := sh.Copy(managedPath, path); err != nil { - return fmt.Errorf("unable to copy %s to %s: %w", path, managedPath, err) - } - - if err := ensureSymlink(managedPath, configPath); err != nil { - return err - } - - return nil - } -} diff --git a/magefiles/config.go b/magefiles/internal/config/config.go similarity index 66% rename from magefiles/config.go rename to magefiles/internal/config/config.go index f974bf2..d9bb9cb 100644 --- a/magefiles/config.go +++ b/magefiles/internal/config/config.go @@ -1,6 +1,4 @@ -//go:build mage - -package main +package config import ( "encoding/json" @@ -10,66 +8,68 @@ import ( "strings" ) -type config struct { +const configDir string = "hosts" + +type Config struct { ManagedConfigurations []string `json:"managedConfigurations"` - BashProfile configBashProfile `json:"bashProfile"` - Directories configDirectories `json:"directories"` - Git configGit `json:"git"` + BashProfile ConfigBashProfile `json:"bashProfile"` + Directories ConfigDirectories `json:"directories"` + Git ConfigGit `json:"git"` } -type configDirectories struct { +type ConfigDirectories struct { UseDefaultDirectories bool `json:"useDefaultDirectories"` IncludeXDGDirectories bool `json:"includeXDGDirectories"` AdditionalDirectories []string `json:"additionalDirectories"` } -type configGit struct { +type ConfigGit struct { GpgSign bool `json:"gpgSign"` - User configGitUser `json:"user"` + User ConfigGitUser `json:"user"` } -type configGitUser struct { +type ConfigGitUser struct { Email string `json:"email"` Name string `json:"name"` SigningKey string `json:"signingKey"` } -type configBashProfile struct { +type ConfigBashProfile struct { Manage bool `json:"manage"` Filename string `json:"filename"` - SessionPaths []configBashProfileSessionPath `json:"sessionPaths"` + SessionPaths []ConfigBashProfileSessionPath `json:"sessionPaths"` XdgDirectories map[string]string `json:"xdgDirectories"` EnvironmentVariables map[string]string `json:"environmentVariables"` Aliases map[string]string `json:"aliases"` - Commands []configBashProfileCommand `json:"commands"` + Commands []ConfigBashProfileCommand `json:"commands"` } -type configBashProfileSessionPath struct { +type ConfigBashProfileSessionPath struct { Path string `json:"path"` Description string `json:"description"` } -type configBashProfileCommand struct { +type ConfigBashProfileCommand struct { Command string `json:"command"` Description string `json:"description"` } -func newConfig() (config, error) { +func NewConfig() (Config, error) { cfg := defaultConfig() path, err := configFilePath() if err != nil { - return config{}, fmt.Errorf("unable to calculate the config file path: %w", err) + return Config{}, fmt.Errorf("unable to calculate the config file path: %w", err) } file, err := os.Open(path) if err != nil { - return config{}, fmt.Errorf("unable to open the file: %w", err) + return Config{}, fmt.Errorf("unable to open the file: %w", err) } defer file.Close() if err = json.NewDecoder(file).Decode(&cfg); err != nil { - return config{}, fmt.Errorf("unable to decode the JSON file: %w", err) + return Config{}, fmt.Errorf("unable to decode the JSON file: %w", err) } return cfg, nil @@ -92,16 +92,16 @@ func configFilePath() (string, error) { return filepath.Join(configDir, identifier+".json"), nil } -func defaultConfig() config { - return config{ - Directories: configDirectories{ +func defaultConfig() Config { + return Config{ + Directories: ConfigDirectories{ UseDefaultDirectories: true, IncludeXDGDirectories: true, AdditionalDirectories: []string{}, }, - Git: configGit{ + Git: ConfigGit{ GpgSign: false, - User: configGitUser{ + User: ConfigGitUser{ Email: "", Name: "", SigningKey: "", diff --git a/magefiles/internal/templatefuncs/env.go b/magefiles/internal/templatefuncs/env.go new file mode 100644 index 0000000..6a84189 --- /dev/null +++ b/magefiles/internal/templatefuncs/env.go @@ -0,0 +1,7 @@ +package templatefuncs + +import "os" + +func Env(value string) string { + return os.Getenv(value) +} diff --git a/magefiles/internal/templatefuncs/templatefuncs.go b/magefiles/internal/templatefuncs/templatefuncs.go new file mode 100644 index 0000000..a40c536 --- /dev/null +++ b/magefiles/internal/templatefuncs/templatefuncs.go @@ -0,0 +1,9 @@ +package templatefuncs + +import "text/template" + +func FuncMap() template.FuncMap { + return template.FuncMap{ + "env": Env, + } +} diff --git a/magefiles/common.go b/magefiles/internal/utilities/utilities.go similarity index 67% rename from magefiles/common.go rename to magefiles/internal/utilities/utilities.go index 2490563..07a20bb 100644 --- a/magefiles/common.go +++ b/magefiles/internal/utilities/utilities.go @@ -1,26 +1,23 @@ -//go:build mage - -package main +package utilities import ( "errors" "fmt" + "html/template" "io/fs" "os" "path/filepath" "slices" + "strings" + + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/config" + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/templatefuncs" ) -const ( - dirModePerm fs.FileMode = 0o700 +const dirModePerm fs.FileMode = 0o700 +const TemplateExtension string = ".gotmpl" - configDir string = "hosts" - rootManagedDir string = "managed" - rootFilesDir string = "files" - rootTemplateDir string = "templates" -) - -func ensureDirectory(path string) error { +func EnsureDirectory(path string) error { info, err := os.Stat(path) if err != nil { if errors.Is(err, os.ErrNotExist) { @@ -50,7 +47,7 @@ func ensureDirectory(path string) error { return nil } -func ensureSymlink(source, dest string) error { +func EnsureSymlink(source, dest string) error { absolutePathErrorMessageFormat := "unable to get the absolute path to %s: %w" absoluteSourcePath, err := filepath.Abs(source) @@ -121,7 +118,7 @@ func ensureSymlink(source, dest string) error { return nil } -func managedConfigSet(applicationConfigurationList []string) map[string]struct{} { +func ManagedConfigSet(applicationConfigurationList []string) map[string]struct{} { set := make(map[string]struct{}) for _, app := range slices.All(applicationConfigurationList) { @@ -130,3 +127,32 @@ func managedConfigSet(applicationConfigurationList []string) map[string]struct{} return set } + +func RenderTemplate(cfg config.Config, templatePath, managedPath string) error { + if !strings.HasSuffix(templatePath, TemplateExtension) { + return fmt.Errorf( + "the template %s does not have the %q file extension", + templatePath, + TemplateExtension, + ) + } + + name := filepath.Base(templatePath) + + tmpl, err := template.New(name).Funcs(templatefuncs.FuncMap()).ParseFiles(templatePath) + if err != nil { + return fmt.Errorf("unable to create a new template value from %s: %w", templatePath, err) + } + + output, err := os.Create(managedPath) + if err != nil { + return fmt.Errorf("unable to create %s: %w", managedPath, err) + } + defer output.Close() + + if err := tmpl.Execute(output, cfg); err != nil { + return fmt.Errorf("unable to render the template to %s: %w", managedPath, err) + } + + return nil +} diff --git a/magefiles/internal/walk/copy_files.go b/magefiles/internal/walk/copy_files.go new file mode 100644 index 0000000..018eb38 --- /dev/null +++ b/magefiles/internal/walk/copy_files.go @@ -0,0 +1,64 @@ +package walk + +import ( + "fmt" + "io/fs" + "path/filepath" + "slices" + "strings" + + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/utilities" + "github.com/magefile/mage/sh" +) + +func CopyFiles( + homeConfigDirectory string, + rootDir string, + rootManagedDir string, + validationFunc func(string) bool, +) fs.WalkDirFunc { + return func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if path == rootDir { + return nil + } + + relativePath := strings.TrimPrefix(path, rootDir+"/") + + if validationFunc != nil { + if !validationFunc(relativePath) { + return nil + } + } + + managedPath := filepath.Join(rootManagedDir, relativePath) + configPath := filepath.Join(homeConfigDirectory, relativePath) + + if d.IsDir() { + dirs := []string{managedPath, configPath} + + for _, dir := range slices.All(dirs) { + if err := utilities.EnsureDirectory(dir); err != nil { + return fmt.Errorf("unable to ensure the existence of the directory %q: %w", dir, err) + } + } + + return nil + } + + fmt.Println("Processing file:", relativePath) + + if err := sh.Copy(managedPath, path); err != nil { + return fmt.Errorf("unable to copy %s to %s: %w", path, managedPath, err) + } + + if err := utilities.EnsureSymlink(managedPath, configPath); err != nil { + return err + } + + return nil + } +} diff --git a/magefiles/internal/walk/render_templates.go b/magefiles/internal/walk/render_templates.go new file mode 100644 index 0000000..a02d1ed --- /dev/null +++ b/magefiles/internal/walk/render_templates.go @@ -0,0 +1,73 @@ +package walk + +import ( + "fmt" + "io/fs" + "path/filepath" + "slices" + "strings" + + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/config" + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/utilities" +) + +func RenderTemplates( + cfg config.Config, + homeConfigDirectory string, + rootDir string, + rootManagedDir string, + validationFunc func(string) bool, +) fs.WalkDirFunc { + return func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + + if path == rootDir { + return nil + } + + relativePath := strings.TrimPrefix(path, rootDir+"/") + + if validationFunc != nil { + if !validationFunc(relativePath) { + return nil + } + } + + if d.IsDir() { + managedDir := filepath.Join(rootManagedDir, relativePath) + configDir := filepath.Join(homeConfigDirectory, relativePath) + + dirs := []string{managedDir, configDir} + + for _, dir := range slices.All(dirs) { + if err := utilities.EnsureDirectory(dir); err != nil { + return fmt.Errorf("unable to ensure the existence of the directory %q: %w", dir, err) + } + } + + return nil + } + + managedPath := filepath.Join(rootManagedDir, strings.TrimSuffix(relativePath, utilities.TemplateExtension)) + configPath := filepath.Join(homeConfigDirectory, strings.TrimSuffix(relativePath, utilities.TemplateExtension)) + + fmt.Println("Processing template:", relativePath) + + if err := utilities.RenderTemplate(cfg, path, managedPath); err != nil { + return fmt.Errorf( + "unable to generate %s from template %s: %w", + managedPath, + path, + err, + ) + } + + if err := utilities.EnsureSymlink(managedPath, configPath); err != nil { + return err + } + + return nil + } +} diff --git a/magefiles/templates.go b/magefiles/templates.go index 6f1ec15..b3799f2 100644 --- a/magefiles/templates.go +++ b/magefiles/templates.go @@ -4,127 +4,59 @@ package main import ( "fmt" - "io/fs" "os" "path/filepath" - "slices" "strings" - "text/template" -) -const templateExtension string = ".gotmpl" + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/config" + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/utilities" + "codeflow.dananglin.me.uk/linux-home/manager/magefiles/internal/walk" +) // Templates generates the configuration files in the managed directory from the templates and // ensures that they the generated files are symlinked correctly to the files in the user's home // configuration directory. func Templates() error { + const rootTemplateDir string = "templates" + homeConfigDirectory, err := os.UserConfigDir() if err != nil { return fmt.Errorf("unable to get the user's home configuration directory: %w", err) } - config, err := newConfig() + cfg, err := config.NewConfig() if err != nil { return fmt.Errorf("unable to load the configuration: %w", err) } - managedConfig := managedConfigSet(config.ManagedConfigurations) + managedConfig := utilities.ManagedConfigSet(cfg.ManagedConfigurations) - if err = filepath.WalkDir(rootTemplateDir, manageTemplatesFunc(homeConfigDirectory, config, managedConfig)); err != nil { - return fmt.Errorf("received an error while processing the templates: %w", err) + validationFunc := func(relativePath string) bool { + split := strings.SplitN(relativePath, "/", 2) + + if len(split) < 1 { + return false + } + + appConfigName := split[0] + + _, exists := managedConfig[appConfigName] + + return exists + } + + if err = filepath.WalkDir( + rootTemplateDir, + walk.RenderTemplates( + cfg, + homeConfigDirectory, + rootTemplateDir, + rootManagedDir, + validationFunc, + ), + ); err != nil { + return fmt.Errorf("received an error while rendering the templates: %w", err) } return nil } - -func manageTemplatesFunc(homeConfigDirectory string, config config, managedConfig map[string]struct{}) fs.WalkDirFunc { - funcMap := template.FuncMap{ - "env": env, - } - - return func(templatePath string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - - if templatePath == rootTemplateDir { - return nil - } - - relativePath := strings.TrimPrefix(templatePath, rootTemplateDir+"/") - - appConfigName := strings.SplitN(relativePath, "/", 2)[0] - - if _, exists := managedConfig[appConfigName]; !exists { - return nil - } - - if d.IsDir() { - managedDir := filepath.Join(rootManagedDir, relativePath) - configDir := filepath.Join(homeConfigDirectory, relativePath) - - dirs := []string{managedDir, configDir} - - for _, dir := range slices.All(dirs) { - if err := ensureDirectory(dir); err != nil { - return fmt.Errorf("unable to ensure the existence of the directory %q: %w", dir, err) - } - } - - return nil - } - - if !strings.HasSuffix(templatePath, templateExtension) { - return fmt.Errorf( - "the template %s does not have the %q file extension", - templatePath, - templateExtension, - ) - } - - managedPath := filepath.Join(rootManagedDir, strings.TrimSuffix(relativePath, templateExtension)) - configPath := filepath.Join(homeConfigDirectory, strings.TrimSuffix(relativePath, templateExtension)) - - fmt.Println("Processing template:", relativePath) - - if err := renderTemplate(config, templatePath, managedPath, funcMap); err != nil { - return fmt.Errorf( - "unable to generate %s from template %s: %w", - managedPath, - templatePath, - err, - ) - } - - if err := ensureSymlink(managedPath, configPath); err != nil { - return err - } - - return nil - } -} - -func renderTemplate(config config, templatePath, managedPath string, funcMap template.FuncMap) error { - name := filepath.Base(templatePath) - - tmpl, err := template.New(name).Funcs(funcMap).ParseFiles(templatePath) - if err != nil { - return fmt.Errorf("unable to create a new template value from %s: %w", templatePath, err) - } - - output, err := os.Create(managedPath) - if err != nil { - return fmt.Errorf("unable to create %s: %w", managedPath, err) - } - defer output.Close() - - if err := tmpl.Execute(output, config); err != nil { - return fmt.Errorf("unable to render the template to %s: %w", managedPath, err) - } - - return nil -} - -func env(value string) string { - return os.Getenv(value) -}