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 }