github.com/lmorg/murex@v0.0.0-20240217211045-e081c89cd4ef/config/profile/module.go (about)

     1  package profile
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"runtime"
     9  	"strings"
    10  
    11  	"github.com/lmorg/murex/app"
    12  	"github.com/lmorg/murex/builtins/pipes/term"
    13  	"github.com/lmorg/murex/lang"
    14  	"github.com/lmorg/murex/lang/ref"
    15  	"github.com/lmorg/murex/shell/autocomplete"
    16  	"github.com/lmorg/murex/utils"
    17  	"github.com/lmorg/murex/utils/ansi"
    18  	"github.com/lmorg/murex/utils/consts"
    19  	"github.com/lmorg/murex/utils/posix"
    20  	"github.com/lmorg/murex/utils/semver"
    21  )
    22  
    23  // Module is the structure for each module within a module's directory.
    24  // Each directory can have multiple modules - this is done so you can separate
    25  // functionality into different logical modules but still keep them inside one
    26  // git repository (or other source control). However I expect the typical usage
    27  // would be one module per repository.
    28  //
    29  // This structure is loaded from module.json file located inside the root of
    30  // the package.
    31  type Module struct {
    32  	Name         string
    33  	Summary      string
    34  	Version      string
    35  	Source       string
    36  	Package      string
    37  	Disabled     bool
    38  	Loaded       bool
    39  	Dependencies Dependencies
    40  }
    41  
    42  // Dependencies is a list of executables required by the module plus a list of
    43  // OSs the module is expected to work against
    44  type Dependencies struct {
    45  	Optional     []string
    46  	Required     []string
    47  	Platform     []string
    48  	MurexVersion string
    49  }
    50  
    51  // Package is some basic details about the package itself as seen in the
    52  // package.json file located at the rood directory inside the package itself
    53  type Package struct {
    54  	Name         string
    55  	Version      string
    56  	Dependencies Dependencies
    57  }
    58  
    59  var (
    60  	// Packages is a struct of all the modules
    61  	Packages = make(map[string][]Module)
    62  
    63  	disabled []string
    64  )
    65  
    66  func isDisabled(name string) bool {
    67  	for i := range disabled {
    68  		if disabled[i] == name {
    69  			return true
    70  		}
    71  	}
    72  
    73  	return false
    74  }
    75  
    76  // Path returns the full path to the murex script that is sourced into your running shell
    77  func (m *Module) Path() string {
    78  	return ModulePath() + m.Package + consts.PathSlash + m.Source
    79  }
    80  
    81  func (m *Module) validate() error {
    82  	var message string
    83  	if strings.TrimSpace(m.Name) == "" {
    84  		message += `  * Property "Name" is empty. This should contain the name of the module` + utils.NewLineString
    85  	}
    86  
    87  	if strings.TrimSpace(m.Summary) == "" {
    88  		message += `  * Property "Summary" is empty. This should contain a brief description of the module` + utils.NewLineString
    89  	}
    90  
    91  	if strings.TrimSpace(m.Version) == "" {
    92  		message += `  * Property "Version" is empty. This should contain a version number of this module` + utils.NewLineString
    93  	}
    94  
    95  	if strings.TrimSpace(m.Source) == "" {
    96  		message += "  * Property \"Source\" is empty. This should contain the name (or path) of the murex script to be `source`ed into your running shell as part of this module" + utils.NewLineString
    97  
    98  	} else {
    99  		fi, err := os.Stat(m.Path())
   100  
   101  		if err != nil {
   102  			message += fmt.Sprintf("  * Unable to stat() script `%s`: %s%s", m.Path(), err.Error(), utils.NewLineString)
   103  
   104  		} else if fi.IsDir() {
   105  			message += fmt.Sprintf("  * Script `%s` exists but is a directory%s", m.Path(), utils.NewLineString)
   106  		}
   107  	}
   108  
   109  	if message != "" {
   110  		return errors.New(message)
   111  	}
   112  
   113  	return m.checkDependencies()
   114  }
   115  
   116  func (m *Module) execute() error {
   117  	file, err := os.OpenFile(m.Path(), os.O_RDONLY, 0640)
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	defer file.Close()
   123  
   124  	b, err := io.ReadAll(file)
   125  	if err != nil {
   126  		return err
   127  	}
   128  
   129  	block := []rune(string(b))
   130  
   131  	os.Stderr.WriteString(fmt.Sprintf("Loading module `%s/%s`%s", m.Package, m.Name, utils.NewLineString))
   132  
   133  	fork := lang.ShellProcess.Fork(lang.F_NEW_MODULE | lang.F_FUNCTION | lang.F_NO_STDIN)
   134  	// lets redirect all output to STDERR just in case this thing gets piped
   135  	// for any strange reason
   136  	fork.Stdout = term.NewErr(false)
   137  	fork.Stderr = term.NewErr(ansi.IsAllowed())
   138  	moduleName := m.Package + "/" + m.Name
   139  	fork.FileRef = &ref.File{Source: &ref.Source{Module: moduleName}}
   140  	fork.FileRef.Source = ref.History.AddSource(m.Path(), moduleName, b)
   141  
   142  	fork.Name.Set("(module)")
   143  	_, err = fork.Execute(block)
   144  	return err
   145  }
   146  
   147  func (m *Module) checkDependencies() error {
   148  	var goos []string
   149  
   150  	if len(m.Dependencies.Platform) == 0 {
   151  		goto checkDeps
   152  	}
   153  
   154  	goos = []string{runtime.GOOS, "any"}
   155  	if posix.IsPosix() {
   156  		goos = append(goos, "posix")
   157  	}
   158  
   159  	for _, supported := range m.Dependencies.Platform {
   160  		for _, host := range goos {
   161  			if host == supported {
   162  				goto checkDeps
   163  			}
   164  		}
   165  	}
   166  
   167  	return errors.New("  * This module isn't designed to run on " + strings.Title(runtime.GOOS))
   168  
   169  checkDeps:
   170  	var message string
   171  
   172  	// check supported version
   173  	if m.Dependencies.MurexVersion != "" {
   174  		ok, err := semver.Compare(app.Version(), m.Dependencies.MurexVersion)
   175  		if err != nil {
   176  			message += "  * Error checking supported Murex version: " + err.Error()
   177  		} else if !ok {
   178  			message += "  * This module is not supported for this version of Murex"
   179  		}
   180  	}
   181  
   182  	// check dependencies
   183  	for _, cmd := range m.Dependencies.Required {
   184  		if !(*autocomplete.GlobalExes.Get())[cmd] && lang.GoFunctions[cmd] == nil && !lang.MxFunctions.Exists(cmd) {
   185  			message += "  * Missing required executable, builtin or murex function: `" + cmd + "`" + utils.NewLineString
   186  		}
   187  	}
   188  
   189  	if message != "" {
   190  		return errors.New(message)
   191  	}
   192  
   193  	return nil
   194  }