eish 0.1.0

Generate self-contained installation scripts (bash, fish, PowerShell) for GitHub release binaries
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
#!/usr/bin/env fish
#
# Installer for <{ slug }>
#
# This file was generated by eish <{ eish_version }> and is not meant to be
# edited: the next time it is regenerated, any change made here is lost. To
# change the installer, change what it is generated from and regenerate.
#
# The complete command that produced this file:
#
#   <{ command }>
#
# Run `./install.fish --help` for the available options.
#

# ---------------------------------------------------------------------------
# Configuration (generated by eish <{ eish_version }>)
# ---------------------------------------------------------------------------
set -g EI_OWNER '<{ owner }>'
set -g EI_REPO '<{ repo }>'
set -g EI_BINARY_NAME '<{ binary }>'

set -q EI_PROXY;          or set -g EI_PROXY '<{ proxy }>'
set -q EI_TAG;            or set -g EI_TAG '<{ tag }>'
set -q EI_DIR;            or set -g EI_DIR (string replace -r '^~' "$HOME" '<{ install_dir }>')
set -q EI_TYPE;           or set -g EI_TYPE '<{ resource_type }>'
set -q EI_REF;            or set -g EI_REF '<{ resource_ref }>'
set -q EI_TARGET;         or set -g EI_TARGET '<{ default_target }>'
set -q EI_MIN_DISK_SPACE; or set -g EI_MIN_DISK_SPACE '<{ min_disk_space_mb }>'
set -q EI_FILE;           or set -g EI_FILE ''

# Target triples bundled in this installer.
set -g EI_SUPPORTED_TARGETS <{ targets | join(" ") }>

# Asset file names bundled in this installer.
set -g EI_SUPPORTED_ASSETS <{ filenames | join(" ") }>

set -g EI_TMP ''

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
function ei_log
    printf '%s\n' $argv >&2
end

function ei_cleanup
    if test -n "$EI_TMP"; and test -d "$EI_TMP"
        rm -rf "$EI_TMP"
        set -g EI_TMP ''
    end
end

function ei_die
    printf 'error: %s\n' $argv >&2
    ei_cleanup
    exit 1
end

function ei_command_exists
    type -q $argv[1]
end

function ei_usage
    printf '%s\n' \
        "Install $EI_BINARY_NAME from GitHub ($EI_OWNER/$EI_REPO)." \
        '' \
        'Usage: install.fish [options]' \
        '' \
        'Options:' \
        '  --proxy <type>     mirror to download through: github, gh-proxy, xget,' \
        "                     jsdelivr, statically   (default: $EI_PROXY)" \
        "  --tag <tag>        release tag to install   (default: $EI_TAG)" \
        "  --dir <path>       installation directory   (default: $EI_DIR)" \
        '  --target <triple>  force a Rust target triple instead of auto-detection' \
        "  --type <type>      resource type: release|file (default: $EI_TYPE)" \
        "  --ref <reference>  branch/tag/commit used by --type file (default: $EI_REF)" \
        '  --file <path>      install from a local file instead of downloading;' \
        '                     its name must match a release asset' \
        '  --list             list the target triples this installer knows' \
        '  -h, --help         show this help' \
        '' \
        'Environment variables mirror the options above:' \
        '  EI_PROXY EI_TAG EI_DIR EI_TARGET EI_TYPE EI_REF EI_MIN_DISK_SPACE EI_FILE' \
        '' \
        'Supported targets:'
    for target in $EI_SUPPORTED_TARGETS
        printf '  %s\n' $target
    end
end

# ---------------------------------------------------------------------------
# Platform table: Rust target triple -> release asset file name
# ---------------------------------------------------------------------------
function ei_platform_filename
    switch $argv[1]<% for asset in assets %>
        case '<{ asset.target }>'
            echo '<{ asset.filename }>'
<%- endfor %>
        case '*'
            return 1
    end
end

# Compatible triples to try, in order, when no exact build exists.
function ei_platform_fallbacks
    switch $argv[1]<% for fallback in fallbacks %>
        case '<{ fallback.target }>'
            echo <{ fallback.alternatives | join(" ") }>
<%- endfor %>
        case '*'
            return 1
    end
    return 0
end

# Targets that can be installed from a given asset file name.
#
# Several targets may share one file, so this returns them all and the caller
# picks.
function ei_platform_targets_for_asset
    switch $argv[1]<% for group in asset_groups %>
        case '<{ group.filename }>'
            echo <{ group.targets | join(" ") }>
<%- endfor %>
        case '*'
            return 1
    end
    return 0
end

# Detect the libc flavour of a GNU/Linux system: prints "musl" or "gnu".
function ei_detect_libc
    for candidate in /bin/sh /usr/bin/ldd /lib/libc.so.6 /lib64/libc.so.6
        test -f $candidate; or continue
        if ei_command_exists strings
            if strings $candidate 2>/dev/null | grep -q musl
                echo musl
                return 0
            end
        end
        if grep -qa musl $candidate 2>/dev/null
            echo musl
            return 0
        end
    end

    for ld in /lib/ld-musl-x86_64.so.1 /lib/ld-musl-aarch64.so.1 /lib/ld-musl-armhf.so.1
        if test -f $ld
            echo musl
            return 0
        end
    end

    if test -f /etc/os-release
        if grep -qi 'alpine\|void' /etc/os-release 2>/dev/null
            echo musl
            return 0
        end
    end

    if ei_command_exists ldd
        if ldd --version 2>&1 | head -n 1 | grep -qi musl
            echo musl
            return 0
        end
    end

    echo gnu
end

# Print the Rust target triple this machine needs.
function ei_detect_platform
    set -l os (uname -s 2>/dev/null; or echo unknown)
    set -l arch (uname -m 2>/dev/null; or echo unknown)

    if test "$os" = Linux
        set -l kernel (uname -o 2>/dev/null; or echo '')
        if test "$kernel" = Android
            switch $arch
                case aarch64 arm64
                    echo aarch64-linux-android
                case 'armv7*' armv8l
                    echo armv7-linux-androideabi
                case i686 x86
                    echo i686-linux-android
                case x86_64 amd64
                    echo x86_64-linux-android
            end
            return 0
        end
    end

    switch $os
        case 'MINGW*' 'MSYS*' 'CYGWIN*' 'Windows*' 'UWIN*'
            switch $arch
                case x86_64 amd64
                    echo x86_64-pc-windows-gnu
                case i686 i386
                    echo i686-pc-windows-gnu
                case aarch64 arm64
                    echo aarch64-pc-windows-msvc
            end
            return 0
        case Darwin
            switch $arch
                case arm64 aarch64
                    echo aarch64-apple-darwin
                case '*'
                    echo x86_64-apple-darwin
            end
            return 0
        case FreeBSD
            if test $arch = x86_64; or test $arch = amd64
                echo x86_64-unknown-freebsd
            end
            return 0
        case NetBSD
            echo x86_64-unknown-netbsd
            return 0
    end

    set -l libc (ei_detect_libc)
    switch $arch
        case x86_64 amd64
            echo "x86_64-unknown-linux-$libc"
        case aarch64 arm64
            echo "aarch64-unknown-linux-$libc"
        case 'armv7*' armv8l
            printf 'armv7-unknown-linux-%seabihf\n' $libc
        case 'arm*'
            printf 'arm-unknown-linux-%seabihf\n' $libc
        case i686 i386
            echo "i686-unknown-linux-$libc"
        case 'riscv64*'
            echo "riscv64gc-unknown-linux-$libc"
        case 'loongarch64*'
            echo "loongarch64-unknown-linux-$libc"
        case ppc64le powerpc64le
            echo powerpc64le-unknown-linux-gnu
        case s390x
            echo s390x-unknown-linux-gnu
    end
end

# Resolve the target triple to install, honouring EI_FILE and EI_TARGET, and
# falling back to a compatible build when the exact one was not published.
function ei_resolve_target
    if test -n "$EI_FILE"
        ei_resolve_target_from_file
        return $status
    end

    set -l primary $EI_TARGET
    if test -z "$primary"
        set primary (ei_detect_platform)
    end
    if test -z "$primary"
        return 1
    end
    if ei_platform_filename $primary >/dev/null 2>&1
        echo $primary
        return 0
    end
    for alternative in (ei_platform_fallbacks $primary 2>/dev/null)
        if ei_platform_filename $alternative >/dev/null 2>&1
            ei_log "no build for $primary, using $alternative instead"
            echo $alternative
            return 0
        end
    end
    return 1
end

# Pick the target to install from the name of a local file.
#
# The file name is the only thing an offline install has to go on, so it has to
# be one of the release's assets. When several targets share that file the
# machine's own platform decides, and only if it is not among them does the
# first one win.
function ei_resolve_target_from_file
    set -l name (basename $EI_FILE)
    set -l matches (ei_platform_targets_for_asset $name 2>/dev/null)

    if test -z "$matches"
        ei_log "known assets: $EI_SUPPORTED_ASSETS"
        ei_die "$name is not an asset of $EI_OWNER/$EI_REPO (see the list above)"
    end

    if test -n "$EI_TARGET"
        if contains -- $EI_TARGET $matches
            echo $EI_TARGET
            return 0
        end
        ei_die "$name is for $matches, not $EI_TARGET"
    end

    set -l detected (ei_detect_platform)
    if contains -- $detected $matches
        echo $detected
        return 0
    end

    ei_log "installing $name for $matches[1] (detected $detected)"
    echo $matches[1]
    return 0
end

function ei_is_windows
    set -l os (uname -s 2>/dev/null; or echo '')
    string match -qr '^(MINGW|MSYS|CYGWIN|Windows|UWIN)' -- $os
end

# Name of a PowerShell interpreter, or nothing if there is none.
#
# 5.1 (`powershell`) and 7+ (`pwsh`) both do everything this script asks of
# them, so whichever is present will do.
function ei_powershell_bin
    if ei_command_exists powershell
        echo powershell
    else if ei_command_exists pwsh
        echo pwsh
    end
end

# The path as a Windows program spells it: backslashes, drive letter.
#
# This is for values a Windows program will consume — the registry, GITHUB_PATH
# — not for anything a person reads; `ei_display_path` covers those. Without
# `cygpath` the path is passed through rather than guessed: `GetFullPath` would
# read `/c/Users/x` as a path on the current drive and produce `C:\c\Users\x`.
function ei_windows_path --argument-names path
    if ei_command_exists cygpath
        cygpath -w $path
        return 0
    end

    echo $path
end

# Expand `~` and make the path absolute.
#
# An absolute path matters because the directory ends up in a shell profile as
# `export PATH="..."`; a relative one would break the next time the shell
# started somewhere else.
#
# This deliberately avoids `cd`: fish runs command substitutions in the current
# shell rather than a subshell, so a `cd` inside one leaks out and would leave
# the caller sitting in the install directory.
function ei_resolve_dir --argument-names path
    set -l raw (string replace -r '^~' "$HOME" -- $path)
    if test -z "$raw"
        return 0
    end

    if type -q path
        set -l resolved (path resolve $raw 2>/dev/null)
        if test -n "$resolved"
            echo $resolved
            return 0
        end
    end

    # GNU `realpath -m` tolerates a directory that does not exist yet; the
    # version without it does not, so it is only used when `path` is missing.
    if command -q realpath
        set -l resolved (realpath -m $raw 2>/dev/null)
        if test -n "$resolved"
            echo $resolved
            return 0
        end
    end

    # Lower on features than `path resolve`, which handles missing directories
    # too; this only catches the remaining case, a relative path with nothing
    # on disk to anchor to.
    if string match -qv '^/' -- $raw; and test -n "$PWD"
        echo "$PWD/$raw"
        return 0
    end

    echo $raw
end

# Render a path the way the platform spells it.
#
# Under MSYS2 `$HOME` is `/c/Users/you`, which no Windows program can use.
# `cygpath -m` gives the mixed form `C:/Users/you`: Windows accepts forward
# slashes, and it is still valid when pasted back into a POSIX shell. Only
# messages go through this — the paths handed to `cp`, `install` and `mkdir`
# keep the shell's own spelling, which is what those tools expect.
#
# Without `cygpath` the path is printed as-is rather than rewritten by hand: a
# `/x/...` path only maps to a drive when `/x` is a mount point, and guessing
# mangles things like `/etc/hosts` into `E://etc/hosts`.
function ei_display_path --argument-names path
    if ei_is_windows; and ei_command_exists cygpath
        cygpath -m $path
        return 0
    end

    echo $path
end

# ---------------------------------------------------------------------------
# Downloading and unpacking
# ---------------------------------------------------------------------------
function ei_download_url --argument-names file
    set -l gh "https://github.com/$EI_OWNER/$EI_REPO"
    set -l xget "https://xget.xi-xu.me/gh/$EI_OWNER/$EI_REPO"

    if test "$EI_TYPE" = file
        switch $EI_PROXY
            case github
                echo "$gh/raw/$EI_REF/$file"
            case gh-proxy
                echo "https://gh-proxy.com/$gh/raw/$EI_REF/$file"
            case xget
                echo "$xget/raw/$EI_REF/$file"
            case jsdelivr
                echo "https://cdn.jsdelivr.net/gh/$EI_OWNER/$EI_REPO@$EI_REF/$file"
            case statically
                echo "https://cdn.statically.io/gh/$EI_OWNER/$EI_REPO/$EI_REF/$file"
        end
        return 0
    end

    if test "$EI_TAG" = latest
        switch $EI_PROXY
            case github
                echo "$gh/releases/latest/download/$file"
            case gh-proxy
                echo "https://gh-proxy.com/$gh/releases/latest/download/$file"
            case xget
                echo "$xget/releases/latest/download/$file"
        end
    else
        switch $EI_PROXY
            case github
                echo "$gh/releases/download/$EI_TAG/$file"
            case gh-proxy
                echo "https://gh-proxy.com/$gh/releases/download/$EI_TAG/$file"
            case xget
                echo "$xget/releases/download/$EI_TAG/$file"
        end
    end
    return 0
end

function ei_download --argument-names url out
    ei_log "downloading $url"

    if ei_command_exists curl
        set -l opts --fail --location --retry 3 --max-time 300
        if test -t 2
            set -a opts --progress-bar
        else
            set -a opts --silent --show-error
        end
        curl $opts --output $out $url
        return $status
    end

    if ei_command_exists wget
        wget --tries=3 --timeout=300 --output-document=$out $url
        return $status
    end

    ei_die 'neither curl nor wget is available; install one of them and retry'
end

function ei_extract --argument-names archive dest name
    switch $name
        case '*.tar.gz' '*.tgz'
            ei_command_exists tar; or ei_die 'tar is required to extract .tar.gz archives'
            tar -xzf $archive -C $dest
        case '*.tar.xz' '*.txz'
            ei_command_exists tar; or ei_die 'tar is required to extract .tar.xz archives'
            tar -xJf $archive -C $dest
        case '*.tar.bz2' '*.tbz2'
            ei_command_exists tar; or ei_die 'tar is required to extract .tar.bz2 archives'
            tar -xjf $archive -C $dest
        case '*.zip'
            if ei_command_exists unzip
                unzip -q -o $archive -d $dest
            else if test -n (ei_powershell_bin)
                # Bound to a variable: fish will not accept a command
                # substitution in command position.
                set -l ps_bin (ei_powershell_bin)
                set -l ps_archive $archive
                set -l ps_dest $dest
                if ei_command_exists cygpath
                    set ps_archive (cygpath -w $archive)
                    set ps_dest (cygpath -w $dest)
                end
                $ps_bin -NoProfile -Command "Expand-Archive -LiteralPath '$ps_archive' -DestinationPath '$ps_dest' -Force"
            else
                ei_die 'unzip (or powershell) is required to extract .zip archives'
            end
        case '*.exe'
            cp $archive "$dest/$EI_BINARY_NAME.exe"
        case '*.gz'
            ei_command_exists gzip; or ei_die 'gzip is required to extract .gz archives'
            gzip -dc $archive > "$dest/$EI_BINARY_NAME"
        case '*'
            cp $archive "$dest/$EI_BINARY_NAME"
    end
end

function ei_find_binary --argument-names dir
    for path in "$dir/$EI_BINARY_NAME" "$dir/$EI_BINARY_NAME.exe" "$dir/bin/$EI_BINARY_NAME" "$dir/bin/$EI_BINARY_NAME.exe"
        if test -f "$path"
            echo "$path"
            return 0
        end
    end

    set -l found (find $dir -maxdepth 3 -type f \( -name "$EI_BINARY_NAME" -o -name "$EI_BINARY_NAME.exe" \) 2>/dev/null | head -n 1)
    if test -n "$found"
        echo "$found"
        return 0
    end

    return 1
end

# ---------------------------------------------------------------------------
# Installation
# ---------------------------------------------------------------------------
function ei_check_disk_space --argument-names dir
    if not string match -qr '^[0-9]+$' -- "$EI_MIN_DISK_SPACE"
        return 0
    end
    if test $EI_MIN_DISK_SPACE -le 0
        return 0
    end

    set -l probe $dir
    while test -n "$probe"; and not test -d "$probe"
        set -l parent (dirname $probe)
        if test "$parent" = "$probe"
            break
        end
        set probe $parent
    end
    test -d "$probe"; or return 0

    set -l free (df -Pk $probe 2>/dev/null | awk 'NR==2 {printf "%d", $4 / 1024}')
    if not string match -qr '^[0-9]+$' -- "$free"
        return 0
    end

    if test $free -lt $EI_MIN_DISK_SPACE
        ei_die (string join '' 'not enough disk space in ' (ei_display_path $dir) ': ' $free 'MB available, ' $EI_MIN_DISK_SPACE 'MB required')
    end
end

function ei_update_path_fish --argument-names dir
    # A PATH entry for a directory that is not there is dead weight: nothing can
    # ever be found through it, and the config only grows.
    if not test -d "$dir"
        ei_log (ei_display_path $dir)" does not exist; leaving PATH alone"
        return 0
    end

    # Case-insensitively: on Windows `Path` and `path` are the same directory,
    # so an exact match would add a second entry that does nothing. The odd
    # false skip on a case-sensitive filesystem needs two directories differing
    # only in case, which is not worth the duplicate it prevents.
    #
    # Both sides are lowered rather than using a flag: for fish's `contains`,
    # `-i` means `--index`, not case-insensitive.
    if contains -- (string lower -- $dir) (string lower -- $PATH)
        ei_log (ei_display_path $dir)" is already on PATH"
        return 0
    end

    set -l cfg "$HOME/.config/fish/config.fish"
    mkdir -p (dirname $cfg) 2>/dev/null

    if test -f $cfg; and grep -Fiq -- $dir $cfg 2>/dev/null
        ei_log "$cfg already mentions "(ei_display_path $dir)
        ei_log "restart your shell (or run: source $cfg) to pick up the change"
        return 0
    end

    # A read-only home directory is unusual but not fatal: the binary is
    # installed either way, so say what to add rather than aborting here.
    if not printf '\n# Added by the %s installer\nfish_add_path %s\n' $EI_BINARY_NAME $dir >> $cfg 2>/dev/null
        ei_log "could not write to $cfg"
        ei_log 'add this to your fish config by hand:'
        ei_log "  fish_add_path $dir"
        return 0
    end

    set -gx PATH $dir $PATH
    ei_log "added "(ei_display_path $dir)" to $cfg"
    ei_log "restart your shell (or run: source $cfg) to pick up the change"
end

function ei_update_path_windows --argument-names dir
    # A PATH entry for a directory that is not there is dead weight: nothing can
    # ever be found through it, and the registry only grows.
    if not test -d "$dir"
        ei_log (ei_display_path $dir)" does not exist; leaving PATH alone"
        return 0
    end

    # Writing a POSIX path into the registry would be worse than doing nothing:
    # nothing on Windows could use it. Without `cygpath` there is no trustworthy
    # way to spell it, so leave the environment alone and say so.
    if not ei_command_exists cygpath
        ei_log "add "(ei_display_path $dir)" to your PATH manually"
        return 0
    end

    # The PATH value keeps the native `C:\...` spelling. Slashes and letter case
    # are normalised on both sides before comparing, because the registry may
    # already hold `C:/x/y` or `c:\X\Y` for the very same directory.
    set -l win_dir (cygpath -w $dir)

    set -l ps (ei_powershell_bin)
    if test -z "$ps"
        ei_log "add "(ei_display_path $dir)" to your PATH manually"
        return 0
    end

    # Prints `present` when the directory is already there, so the message can
    # say which of the two things happened.
    set -l result ($ps -NoProfile -Command "\$native = '$win_dir'.TrimEnd('\'); \$p = [Environment]::GetEnvironmentVariable('Path', 'User'); \$found = \$false; foreach (\$e in (\$p -split ';')) { if (\$e -and (\$e -replace '/','\').TrimEnd('\') -ieq \$native) { \$found = \$true; break } }; if (\$found) { 'present' } else { [Environment]::SetEnvironmentVariable('Path', \"\$p;\$native\", 'User') }" 2>/dev/null)
    if test $status -ne 0
        ei_log 'could not update the Windows PATH automatically'
        ei_log "add "(ei_display_path $dir)" to your PATH manually"
        return 0
    end

    if test "$result" = present
        ei_log (ei_display_path $dir)" is already on the user PATH"
        return 0
    end

    ei_log "added "(ei_display_path $dir)" to the user PATH"
    ei_log 'restart your terminal to pick up the change'
end

# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
function ei_validate_config
    switch $EI_PROXY
        case github gh-proxy xget jsdelivr statically
        case '*'
            ei_die "unknown proxy: $EI_PROXY"
    end

    switch $EI_TYPE
        case release file
        case '*'
            ei_die "unknown resource type: $EI_TYPE (expected release or file)"
    end

    if test "$EI_TYPE" = release
        switch $EI_PROXY
            case jsdelivr statically
                # Only worth complaining about when something will actually be
                # downloaded: an offline install never builds a URL.
                if test -z "$EI_FILE"
                    ei_die "the $EI_PROXY proxy cannot serve release assets; use --proxy github or --type file"
                end
        end
    end

    if test -n "$EI_FILE"; and not test -f "$EI_FILE"
        ei_die "no such file: "(ei_display_path $EI_FILE)
    end
end

function ei_main
    argparse -n install.fish 'p/proxy=' 't/tag=' 'd/dir=' 'T/target=' 'type=' 'ref=' 'file=' 'list' 'h/help' -- $argv
    or return 1

    if set -q _flag_help
        ei_usage
        return 0
    end

    if set -q _flag_proxy
        set -g EI_PROXY $_flag_proxy
    end
    if set -q _flag_tag
        set -g EI_TAG $_flag_tag
    end
    if set -q _flag_dir
        set -g EI_DIR (ei_resolve_dir $_flag_dir)
    end
    if set -q _flag_target
        set -g EI_TARGET $_flag_target
    end
    if set -q _flag_type
        set -g EI_TYPE $_flag_type
    end
    if set -q _flag_ref
        set -g EI_REF $_flag_ref
    end
    if set -q _flag_file
        set -g EI_FILE $_flag_file
    end

    # `~` in the default and any relative `--dir` both become absolute here, so
    # that whatever lands in a shell profile is usable from anywhere.
    set -g EI_DIR (ei_resolve_dir $EI_DIR)

    if set -q _flag_list
        for target in $EI_SUPPORTED_TARGETS
            echo $target
        end
        return 0
    end

    # After the list check: `--list` is a question about this installer, not
    # about a release, so it answers even with an unusable proxy or a missing
    # --file.
    ei_validate_config

    set -l target (ei_resolve_target)
    if test $status -ne 0; or test -z "$target"
        ei_log "supported targets: $EI_SUPPORTED_TARGETS"
        if test -n "$EI_TARGET"
            ei_die "no build for target $EI_TARGET (check --target, or drop it to auto-detect)"
        end
        ei_die "no prebuilt binary for "(uname -s)"/"(uname -m)
    end

    set -l filename (ei_platform_filename $target)
    if test -n "$EI_FILE"
        ei_log "installing $EI_BINARY_NAME ($target) from "(ei_display_path $EI_FILE)
    else
        ei_log "installing $EI_BINARY_NAME ($target) into "(ei_display_path $EI_DIR)
    end

    ei_check_disk_space $EI_DIR

    set -g EI_TMP (mktemp -d 2>/dev/null; or echo "$TMPDIR/eish-$fish_pid")
    mkdir -p $EI_TMP

    # A local file is used where it lies, so it is never copied or deleted.
    set -l archive "$EI_TMP/$filename"
    if test -n "$EI_FILE"
        set archive $EI_FILE
    else
        ei_download (ei_download_url $filename) $archive
        or ei_die "download failed"
    end

    ei_extract $archive "$EI_TMP" "$filename"
    or ei_die "could not extract $filename"

    set -l binary (ei_find_binary $EI_TMP)
    if test $status -ne 0; or test -z "$binary"
        ei_die "could not find $EI_BINARY_NAME inside $filename"
    end

    mkdir -p $EI_DIR
    set -l stem $EI_BINARY_NAME
    if ei_is_windows
        set stem "$EI_BINARY_NAME.exe"
    end

    if ei_command_exists install
        install -m 0755 $binary "$EI_DIR/$stem"
    else
        cp $binary "$EI_DIR/$stem"
        chmod 0755 "$EI_DIR/$stem"
    end
    or ei_die "could not install to "(ei_display_path "$EI_DIR/$stem")

    ei_log "installed "(ei_display_path "$EI_DIR/$stem")

    if ei_is_windows
        ei_update_path_windows $EI_DIR
    else
        ei_update_path_fish $EI_DIR
    end

    if set -q GITHUB_PATH
        # The file is read by the CI runner, not by a person, so it gets the
        # platform's own spelling: a POSIX path in a Windows runner's PATH is
        # useless to everything but MSYS itself.
        if ei_is_windows
            echo (ei_windows_path $EI_DIR) >> $GITHUB_PATH
        else
            echo $EI_DIR >> $GITHUB_PATH
        end
        ei_log "added "(ei_display_path $EI_DIR)" to GITHUB_PATH"
    end

    ei_cleanup
    return 0
end

ei_main $argv