github.com/markusbkk/elvish@v0.0.0-20231204143114-91dc52438621/pkg/mods/epm/epm.elv (about)

     1  use re
     2  use str
     3  use platform
     4  
     5  # Verbosity configuration
     6  var debug-mode = $false
     7  
     8  # Configuration for common domains
     9  var -default-domain-config = [
    10    &"github.com"= [
    11      &method= git
    12      &protocol= https
    13      &levels= 2
    14    ]
    15    &"bitbucket.org"= [
    16      &method= git
    17      &protocol= https
    18      &levels= 2
    19    ]
    20    &"gitlab.com"= [
    21      &method= git
    22      &protocol= https
    23      &levels= 2
    24    ]
    25  ]
    26  
    27  #elvdoc:var managed-dir
    28  #
    29  # The path of the `epm`-managed directory.
    30  
    31  var managed-dir = (
    32    if $platform:is-windows {
    33      put $E:LocalAppData/elvish/lib
    34    } elif (not-eq $E:XDG_DATA_HOME '') {
    35      put $E:XDG_DATA_HOME/elvish/lib
    36    } else {
    37      put ~/.local/share/elvish/lib
    38    }
    39  )
    40  
    41  # General utility functions
    42  
    43  fn -debug {|text|
    44    if $debug-mode {
    45      print (styled '=> ' blue)
    46      echo $text
    47    }
    48  }
    49  
    50  fn -info {|text|
    51    print (styled '=> ' green)
    52    echo $text
    53  }
    54  
    55  fn -warn {|text|
    56    print (styled '=> ' yellow)
    57    echo $text
    58  }
    59  
    60  fn -error {|text|
    61    print (styled '=> ' red)
    62    echo $text
    63  }
    64  
    65  fn dest {|pkg|
    66    put $managed-dir/$pkg
    67  }
    68  
    69  #elvdoc:fn is-installed
    70  #
    71  # ```elvish
    72  # epm:is-installed $pkg
    73  # ```
    74  #
    75  # Returns a boolean value indicating whether the given package is installed.
    76  
    77  fn is-installed {|pkg|
    78    bool ?(test -e (dest $pkg))
    79  }
    80  
    81  fn -package-domain {|pkg|
    82    str:split &max=2 / $pkg | take 1
    83  }
    84  
    85  fn -package-without-domain {|pkg|
    86    str:split &max=2 / $pkg | drop 1 | str:join ''
    87  }
    88  
    89  # Merge two maps
    90  fn -merge {|a b|
    91    keys $b | each {|k| set a[$k] = $b[$k] }
    92    put $a
    93  }
    94  
    95  # Uppercase first letter of a string
    96  fn -first-upper {|s|
    97    put (echo $s[0] | tr '[:lower:]' '[:upper:]')$s[(count $s[0]):]
    98  }
    99  
   100  # Expand tilde at the beginning of a string to the home dir
   101  fn -tilde-expand {|p|
   102    re:replace "^~" $E:HOME $p
   103  }
   104  
   105  # Known method handlers. Each entry is indexed by method name (the
   106  # value of the "method" key in the domain configs), and must contain
   107  # two keys: install and upgrade, each one must be a closure that
   108  # receives two arguments: package name and the domain config entry
   109  #
   110  # - Method 'git' requires the key 'protocol' in the domain config,
   111  #   which has to be 'http' or 'https'
   112  # - Method 'rsync' requires the key 'location' in the domain config,
   113  #   which has to contain the directory where the domain files are
   114  #   stored. It can be any source location understood by the rsync
   115  #   command.
   116  var -method-handler
   117  set -method-handler = [
   118    &git= [
   119      &src= {|pkg dom-cfg|
   120        put $dom-cfg[protocol]"://"$pkg
   121      }
   122  
   123      &install= {|pkg dom-cfg|
   124        var dest = (dest $pkg)
   125        -info "Installing "$pkg
   126        mkdir -p $dest
   127        git clone ($-method-handler[git][src] $pkg $dom-cfg) $dest
   128      }
   129  
   130      &upgrade= {|pkg dom-cfg|
   131        var dest = (dest $pkg)
   132        -info "Updating "$pkg
   133        try {
   134          git -C $dest pull
   135        } except _ {
   136            -error "Something failed, please check error above and retry."
   137        }
   138      }
   139    ]
   140  
   141    &rsync= [
   142      &src= {|pkg dom-cfg|
   143        put (-tilde-expand $dom-cfg[location])/(-package-without-domain $pkg)/
   144      }
   145  
   146      &install= {|pkg dom-cfg|
   147        var dest = (dest $pkg)
   148        var pkgd = (-package-without-domain $pkg)
   149        -info "Installing "$pkg
   150        rsync -av ($-method-handler[rsync][src] $pkg $dom-cfg) $dest
   151      }
   152  
   153      &upgrade= {|pkg dom-cfg|
   154        var dest = (dest $pkg)
   155        var pkgd = (-package-without-domain $pkg)
   156        if (not (is-installed $pkg)) {
   157          -error "Package "$pkg" is not installed."
   158          return
   159        }
   160        -info "Updating "$pkg
   161        rsync -av ($-method-handler[rsync][src] $pkg $dom-cfg) $dest
   162      }
   163    ]
   164  ]
   165  
   166  # Return the filename of the domain config file for the given domain
   167  # (regardless of whether it exists)
   168  fn -domain-config-file {|dom|
   169    put $managed-dir/$dom/epm-domain.cfg
   170  }
   171  
   172  # Return the filename of the metadata file for the given package
   173  # (regardless of whether it exists)
   174  fn -package-metadata-file {|pkg|
   175    put (dest $pkg)/metadata.json
   176  }
   177  
   178  fn -write-domain-config {|dom|
   179    var cfgfile = (-domain-config-file $dom)
   180    mkdir -p (dirname $cfgfile)
   181    if (has-key $-default-domain-config $dom) {
   182      put $-default-domain-config[$dom] | to-json > $cfgfile
   183    } else {
   184      -error "No default config exists for domain "$dom"."
   185    }
   186  }
   187  
   188  # Returns the domain config for a given domain parsed from JSON.
   189  # If the file does not exist but we have a built-in
   190  # definition, then we return the default. Otherwise we return $false,
   191  # so the result can always be checked with 'if'.
   192  fn -domain-config {|dom|
   193    var cfgfile = (-domain-config-file $dom)
   194    var cfg = $false
   195    if ?(test -f $cfgfile) {
   196      # If the config file exists, read it...
   197      set cfg = (cat $cfgfile | from-json)
   198      -debug "Read domain config for "$dom": "(to-string $cfg)
   199    } else {
   200      # ...otherwise check if we have a default config for the domain, and save it
   201      if (has-key $-default-domain-config $dom) {
   202        set cfg = $-default-domain-config[$dom]
   203        -debug "No existing config for "$dom", using the default: "(to-string $cfg)
   204      } else {
   205        -debug "No existing config for "$dom" and no default available."
   206      }
   207    }
   208    put $cfg
   209  }
   210  
   211  
   212  # Return the method by which a package is installed
   213  fn -package-method {|pkg|
   214    var dom = (-package-domain $pkg)
   215    var cfg = (-domain-config $dom)
   216    if $cfg {
   217      put $cfg[method]
   218    } else {
   219      put $false
   220    }
   221  }
   222  
   223  # Invoke package operations defined in $-method-handler above
   224  fn -package-op {|pkg what|
   225    var dom = (-package-domain $pkg)
   226    var cfg = (-domain-config $dom)
   227    if $cfg {
   228      var method = $cfg[method]
   229      if (has-key $-method-handler $method) {
   230        if (has-key $-method-handler[$method] $what) {
   231          $-method-handler[$method][$what] $pkg $cfg
   232        } else {
   233          fail "Unknown operation '"$what"' for package "$pkg
   234        }
   235      } else {
   236        fail "Unknown method '"$method"', specified in in config file "(-domain-config-file $dom)
   237      }
   238    } else {
   239      -error "No config for domain '"$dom"'."
   240    }
   241  }
   242  
   243  # Uninstall a single package by removing its directory
   244  fn -uninstall-package {|pkg|
   245    if (not (is-installed $pkg)) {
   246      -error "Package "$pkg" is not installed."
   247      return
   248    }
   249    var dest = (dest $pkg)
   250    -info "Removing package "$pkg
   251    rm -rf $dest
   252  }
   253  
   254  ######################################################################
   255  # Main user-facing functions
   256  
   257  #elvdoc:fn metadata
   258  #
   259  # ```elvish
   260  # epm:metadata $pkg
   261  # ```
   262  #
   263  # Returns a hash containing the metadata for the given package. Metadata for a
   264  # package includes the following base attributes:
   265  #
   266  # -   `name`: name of the package
   267  # -   `installed`: a boolean indicating whether the package is currently installed
   268  # -   `method`: method by which it was installed (`git` or `rsync`)
   269  # -   `src`: source URL of the package
   270  # -   `dst`: where the package is (or would be) installed. Note that this
   271  #     attribute is returned even if `installed` is `$false`.
   272  #
   273  # Additionally, packages can define arbitrary metadata attributes in a file called
   274  # `metadata.json` in their top directory. The following attributes are
   275  # recommended:
   276  #
   277  # -   `description`: a human-readable description of the package
   278  # -   `maintainers`: an array containing the package maintainers, in
   279  #     `Name <email>` format.
   280  # -   `homepage`: URL of the homepage for the package, if it has one.
   281  # -   `dependencies`: an array listing dependencies of the current package. Any
   282  #     packages listed will be installed automatically by `epm:install` if they are
   283  #     not yet installed.
   284  
   285  # Read and parse the package metadata, if it exists
   286  fn metadata {|pkg|
   287    # Base metadata attributes
   288    var res = [
   289      &name= $pkg
   290      &method= (-package-method $pkg)
   291      &src= (-package-op $pkg src)
   292      &dst= (dest $pkg)
   293      &installed= (is-installed $pkg)
   294    ]
   295    # Merge with package-specified attributes, if any
   296    var file = (-package-metadata-file $pkg)
   297    if (and (is-installed $pkg) ?(test -f $file)) {
   298      set res = (-merge (cat $file | from-json) $res)
   299    }
   300    put $res
   301  }
   302  
   303  #elvdoc:fn query
   304  #
   305  # ```elvish
   306  # epm:query $pkg
   307  # ```
   308  #
   309  # Pretty print the available metadata of the given package.
   310  
   311  # Print out information about a package
   312  fn query {|pkg|
   313    var data = (metadata $pkg)
   314    var special-keys = [name method installed src dst]
   315    echo (styled "Package "$data[name] cyan)
   316    if $data[installed] {
   317      echo (styled "Installed at "$data[dst] green)
   318    } else {
   319      echo (styled "Not installed" red)
   320    }
   321    echo (styled "Source:" blue) $data[method] $data[src]
   322    keys $data | each {|key|
   323      if (not (has-value $special-keys $key)) {
   324        var val = $data[$key]
   325        if (eq (kind-of $val) list) {
   326          set val = (str:join ", " $val)
   327        }
   328        echo (styled (-first-upper $key)":" blue) $val
   329      }
   330    }
   331  }
   332  
   333  #elvdoc:fn installed
   334  #
   335  # ```elvish
   336  # epm:installed
   337  # ```
   338  #
   339  # Return an array with all installed packages. `epm:list` can be used as an alias
   340  # for `epm:installed`.
   341  
   342  # List installed packages
   343  fn installed {
   344    put $managed-dir/*[nomatch-ok] | each {|dir|
   345      var dom = (str:replace $managed-dir/ '' $dir)
   346      var cfg = (-domain-config $dom)
   347      # Only list domains for which we know the config, so that the user
   348      # can have his own non-package directories under ~/.elvish/lib
   349      # without conflicts.
   350      if $cfg {
   351        var lvl = $cfg[levels]
   352        var pat = '^\Q'$managed-dir'/\E('(repeat (+ $lvl 1) '[^/]+' | str:join '/')')/$'
   353        put (each {|d| re:find $pat $d } [ $managed-dir/$dom/**[nomatch-ok]/ ] )[groups][1][text]
   354      }
   355    }
   356  }
   357  
   358  # epm:list is an alias for epm:installed
   359  fn list { installed }
   360  
   361  #elvdoc:fn install
   362  #
   363  # ```elvish
   364  # epm:install &silent-if-installed=$false $pkg...
   365  # ```
   366  #
   367  # Install the named packages. By default, if a package is already installed, a
   368  # message will be shown. This can be disabled by passing
   369  # `&silent-if-installed=$true`, so that already-installed packages are silently
   370  # ignored.
   371  
   372  # Install and upgrade are method-specific, so we call the
   373  # corresponding functions using -package-op
   374  fn install {|&silent-if-installed=$false @pkgs|
   375    if (eq $pkgs []) {
   376      -error "You must specify at least one package."
   377      return
   378    }
   379    for pkg $pkgs {
   380      if (is-installed $pkg) {
   381        if (not $silent-if-installed) {
   382          -info "Package "$pkg" is already installed."
   383        }
   384      } else {
   385        -package-op $pkg install
   386        # Check if there are any dependencies to install
   387        var metadata = (metadata $pkg)
   388        if (has-key $metadata dependencies) {
   389          var deps = $metadata[dependencies]
   390          -info "Installing dependencies: "(str:join " " $deps)
   391          # If the installation of dependencies fails, uninstall the
   392          # target package (leave any already-installed dependencies in
   393          # place)
   394          try {
   395            install $@deps
   396          } except e {
   397            -error "Dependency installation failed. Uninstalling "$pkg", please check the errors above and try again."
   398            -uninstall-package $pkg
   399          }
   400        }
   401      }
   402    }
   403  }
   404  
   405  #elvdoc:fn upgrade
   406  #
   407  # ```elvish
   408  # epm:upgrade $pkg...
   409  # ```
   410  #
   411  # Upgrade named packages. If no package name is given, upgrade all installed
   412  # packages.
   413  
   414  fn upgrade {|@pkgs|
   415    if (eq $pkgs []) {
   416      set pkgs = [(installed)]
   417      -info 'Upgrading all installed packages'
   418    }
   419    for pkg $pkgs {
   420      if (not (is-installed $pkg)) {
   421        -error "Package "$pkg" is not installed."
   422      } else {
   423        -package-op $pkg upgrade
   424      }
   425    }
   426  }
   427  
   428  #elvdoc:fn uninstall
   429  #
   430  # ```elvish
   431  # epm:uninstall $pkg...
   432  # ```
   433  #
   434  # Uninstall named packages.
   435  
   436  # Uninstall is the same for everyone, just remove the directory
   437  fn uninstall {|@pkgs|
   438    if (eq $pkgs []) {
   439      -error 'You must specify at least one package.'
   440      return
   441    }
   442    for pkg $pkgs {
   443      -uninstall-package $pkg
   444    }
   445  }