github.com/nats-io/nsc@v0.0.0-20221206222106-35db9400b257/install.sh (about)

     1  #!/bin/sh
     2  set -eu
     3  
     4  # Copyright 2020-2022 The NATS Authors
     5  # Licensed under the Apache License, Version 2.0 (the "License");
     6  # you may not use this file except in compliance with the License.
     7  # You may obtain a copy of the License at
     8  #
     9  # http://www.apache.org/licenses/LICENSE-2.0
    10  #
    11  # Unless required by applicable law or agreed to in writing, software
    12  # distributed under the License is distributed on an "AS IS" BASIS,
    13  # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14  # See the License for the specific language governing permissions and
    15  # limitations under the License.
    16  
    17  # We are sh, not bash; we might want bash/zsh for associative arrays but some
    18  # OSes are currently on bash3 and removing bash, while we don't want a zsh
    19  # dependency; so we're sticking to "pretty portable shell" even if it's a
    20  # little more convoluted as a result.
    21  #
    22  # We rely upon the following beyond basic POSIX shell:
    23  #  1. A  `local`  command (built-in to shell; almost all sh does have this)
    24  #  2. A  `curl`   command (to download files)
    25  #  3. An `unzip`  command (to extract content from .zip files)
    26  #  4. A  `mktemp` command (to stage a downloaded zip-file)
    27  
    28  # We rely upon naming conventions for release assets from GitHub to avoid
    29  # having to parse JSON from their API, to avoid a dependency upon jq(1).
    30  #
    31  # <https://help.github.com/en/github/administering-a-repository/linking-to-releases>
    32  # guarantees that:
    33  #    /<owner>/<name>/releases/latest/download/<asset-name>.zip
    34  # will be available; going via the API we got, for 'latest':
    35  #    https://github.com/nats-io/nsc/releases/download/0.4.0/nsc-linux-amd64.zip
    36  # ie:
    37  #    /<owner>/<name>/releases/download/<release>/<asset-name>.zip
    38  # Inspecting headers, the documented guarantee redirects to the API-returned
    39  # URL, which redirects to the S3 bucket download URL.
    40  
    41  # This is a list of the architectures we support, which should be listed in
    42  # the Go architecture naming format.
    43  readonly SUPPORTED_ARCHS="amd64 arm64"
    44  
    45  # Finding the releases to download
    46  readonly GITHUB_OWNER_REPO='nats-io/nsc'
    47  readonly HTTP_USER_AGENT='nsc_install/0.2 (@nats-io)'
    48  
    49  # Where to install to, relative to home-dir
    50  readonly NSC_RELATIVE_BIN_DIR='.nsccli/bin'
    51  # Binary name we are looking for (might have .exe extension on some platforms)
    52  readonly NSC_BINARY_BASENAME='nsc'
    53  
    54  progname="$(basename "$0" .sh)"
    55  note() { printf >&2 '%s: %s\n' "$progname" "$*"; }
    56  die() { note "$@"; exit 1; }
    57  
    58  main() {
    59    parse_options "$@"
    60    shift $((OPTIND - 1))
    61    # error early if missing commands; put it after option processing
    62    # so that if we need to, we can add options to handle alternatives.
    63    check_have_external_commands
    64  
    65    # mkdir -m does not set permissions of parents; -v is not portable
    66    # We don't create anything private, so stick to inherited umask.
    67    mkdir -p -- "$opt_install_dir"
    68  
    69    zipfile_url="$(determine_zip_download_url)"
    70    [ -n "${zipfile_url}" ] || die "unable to determine a download URL"
    71    want_filename="$(exe_filename_per_os)"
    72  
    73    # The unzip command does not work well with piped stdin, we need to have
    74    # the complete zip-file on local disk.  The portability of mktemp(1) is
    75    # an unpleasant situation.
    76    # This is the sanest way to get a temporary directory which only we can
    77    # even look inside.
    78    old_umask="$(umask)"
    79    umask 077
    80    zip_dir="$(mktemp -d 2>/dev/null || mktemp -d -t 'ziptmpdir')" || \
    81      die "failed to create a temporary directory with mktemp(1)"
    82    umask "$old_umask"
    83    # POSIX does not give rm(1) a `-v` flag.
    84    trap "rm -rf -- '${zip_dir}'" EXIT
    85  
    86    stage_zipfile="${zip_dir}/$(zip_filename_per_os)"
    87  
    88    note "Downloading <${zipfile_url}>"
    89    curl_cmd --progress-bar --location --output "$stage_zipfile" "$zipfile_url"
    90  
    91    note "Extracting ${want_filename} from $stage_zipfile"
    92    # But unzip(1) does not let us override permissions and it does not obey
    93    # umask so the file might now exist with overly broad permissions, depending
    94    # upon whether or not the local environment has a per-user group which was
    95    # used.  We don't know that the extracting user wants everyone else in their
    96    # current group to be able to write to the file.
    97    # So: extract into the temporary directory, which we've forced via umask to
    98    # be self-only, chmod while it's safe in there, and then move it into place.
    99    #   -b is not in busybox, so we rely on unzip handling binary safely
   100    #   -j junks paths inside the zipfile; none expected, enforce that
   101    unzip -j -d "$zip_dir" "$stage_zipfile" "$want_filename"
   102    chmod 0755 "$zip_dir/$want_filename"
   103    # prompt the user to overwrite if need be
   104    mv -i -- "$zip_dir/$want_filename" "$opt_install_dir/./"
   105  
   106    link_and_show_instructions "$want_filename"
   107  }
   108  
   109  usage() {
   110    local ev="${1:-1}"
   111    [ "$ev" = 0 ] || exec >&2
   112    cat <<EOUSAGE
   113  Usage: $progname [-t <tag>] [-d <dir>] [-s <dir>]
   114   -d dir     directory to download into [default: ~/$NSC_RELATIVE_BIN_DIR]
   115   -s dir     directory in which to place a symlink to the binary
   116              [default: ~/bin] [use '-' to forcibly not place a symlink]
   117   -t tag     retrieve a tagged release instead of the latest
   118   -a arch    force choosing a specific processor architecture [allowed: $SUPPORTED_ARCHS]
   119  EOUSAGE
   120    exit "$ev"
   121  }
   122  
   123  opt_tag=''
   124  opt_install_dir=''
   125  opt_symlink_dir=''
   126  opt_arch=''
   127  parse_options() {
   128    while getopts ':a:d:hs:t:' arg; do
   129      case "$arg" in
   130        (h) usage 0 ;;
   131  
   132        (d) opt_install_dir="$OPTARG" ;;
   133        (s) opt_symlink_dir="$OPTARG" ;;
   134        (t) opt_tag="$OPTARG" ;;
   135  
   136        (a)
   137          if validate_arch "$OPTARG"; then
   138            opt_arch="$OPTARG"
   139          else
   140            die "unsupported arch for -a, try one of: $SUPPORTED_ARCHS"
   141          fi ;;
   142  
   143        (:) die "missing required option for -$OPTARG; see -h for help" ;;
   144        (\?) die "unknown option -$OPTARG; see -h for help" ;;
   145        (*) die "unhandled option -$arg; CODE BUG" ;;
   146      esac
   147    done
   148  
   149    if [ "$opt_install_dir" = "" ]; then
   150      opt_install_dir="${HOME:?}/${NSC_RELATIVE_BIN_DIR}"
   151    fi
   152    if [ "$opt_symlink_dir" = "" ] && [ -d "$HOME/bin" ]; then
   153      opt_symlink_dir="$HOME/bin"
   154    elif [ "$opt_symlink_dir" = "-" ]; then
   155      opt_symlink_dir=""
   156    fi
   157  }
   158  
   159  check_have_external_commands() {
   160    local cmd
   161  
   162    # Only those commands which take --help :
   163    for cmd in curl unzip
   164    do
   165      "$cmd" --help >/dev/null || die "missing command: $cmd"
   166    done
   167  
   168    # Our invocation of mktemp has to handle multiple variants; if that's not
   169    # installed, let it fail later.
   170  
   171    test -e /dev/stdin || die "missing device /dev/stdin"
   172  }
   173  
   174  normalized_ostype() {
   175    local ostype
   176    # We only need to worry about ASCII here
   177    ostype="$(uname -s | tr A-Z a-z)"
   178    case "$ostype" in
   179      (*linux*)  ostype="linux" ;;
   180      (win32)    ostype="windows" ;;
   181      (ming*_nt) ostype="windows" ;;
   182    esac
   183    printf '%s\n' "$ostype"
   184  }
   185  
   186  validate_arch() {
   187    local check="$1"
   188    local x
   189    # Deliberately not quoted, setting $@ within this function
   190    set $SUPPORTED_ARCHS
   191    for x; do
   192      if [ "$x" = "$check" ]; then
   193        return 0
   194      fi
   195    done
   196    return 1
   197  }
   198  
   199  normalized_arch() {
   200    # We are normalising to the Golang nomenclature, which is how the binaries are released.
   201    # The main ones are:  amd64 arm64
   202    # There is no universal standard here.  Go's is as good as any.
   203  
   204    # Command-line flag is the escape hatch.
   205    if [ -n "${opt_arch:-}" ]; then
   206      printf '%s\n' "$opt_arch"
   207      return 0
   208    fi
   209  
   210    # Beware `uname -m` vs `uname -p`.
   211    # Nominally, -m is machine, -p is processor.  But what does this mean in practice?
   212    # In practice, -m tends to be closer to the absolute truth of what the CPU is,
   213    # while -p is adaptive to personality, binary type, etc.
   214    # On Alpine Linux inside Docker, -p can fail `unknown` while `-m` works.
   215    #
   216    #                 uname -m    uname -p
   217    # Darwin/x86      x86_64      i386
   218    # Darwin/M1       arm64       arm
   219    # Alpine/docker   x86_64      unknown
   220    # Ubuntu/x86/64b  x86_64      x86_64
   221    # RPi 3B Linux    armv7l      unknown     (-m depends upon boot flags & kernel)
   222    #
   223    # SUSv4 requires that uname exist and that it have the -m flag, but does not document -p.
   224    local narch
   225    narch="$(uname -m)"
   226    case "$narch" in
   227      (x86_64) narch="amd64" ;;
   228      (amd64) true ;;
   229      (aarch64) narch="arm64" ;;
   230      (arm64) true ;;
   231      (*) die "Unhandled architecture '$narch', use -a flag to select a supported arch" ;;
   232    esac
   233    if validate_arch "$narch"; then
   234      printf '%s\n' "$narch"
   235    else
   236      die "Unhandled architecture '$narch', use -a flag to select a supported arch"
   237    fi
   238  }
   239  
   240  zip_filename_per_os() {
   241    # We break these out into a separate variable instead of passing directly
   242    # to printf, so that if there's a failure in normalization then the printf
   243    # won't swallow the exit status of the $(...) subshell and will instead
   244    # abort correctly.
   245    local zipname
   246    zipname="nsc-$(normalized_ostype)-$(normalized_arch).zip" || exit $?
   247    printf '%s\n' "${zipname}"
   248  }
   249  
   250  exe_filename_per_os() {
   251    local fn="$NSC_BINARY_BASENAME"
   252    case "$(normalized_ostype)" in
   253      (windows) fn="${fn}.exe" ;;
   254    esac
   255    printf '%s\n' "$fn"
   256  }
   257  
   258  curl_cmd() {
   259    curl --user-agent "$HTTP_USER_AGENT" "$@"
   260  }
   261  
   262  determine_zip_download_url() {
   263    local want_filename download_url
   264  
   265    want_filename="$(zip_filename_per_os)"
   266    if [ -n "$opt_tag" ]; then
   267      printf 'https://github.com/%s/releases/download/%s/%s\n' \
   268        "$GITHUB_OWNER_REPO" "$opt_tag" "$want_filename"
   269    else
   270      printf 'https://github.com/%s/releases/latest/download/%s\n' \
   271        "$GITHUB_OWNER_REPO" "$want_filename"
   272    fi
   273  }
   274  
   275  dir_is_in_PATH() {
   276    local needle="$1"
   277    local oIFS="$IFS"
   278    local pathdir
   279    case "$(normalized_ostype)" in
   280      (windows) IFS=';' ;;
   281      (*)       IFS=':' ;;
   282    esac
   283    set $PATH
   284    IFS="$oIFS"
   285    for pathdir
   286    do
   287      if [ "$pathdir" = "$needle" ]; then
   288        return 0
   289      fi
   290    done
   291    return 1
   292  }
   293  
   294  # Returns true if no further installation instructions are needed;
   295  # Returns false otherwise.
   296  maybe_make_symlink() {
   297    local target="${1:?need a file to link to}"
   298    local symdir="${2:?need a directory within which to create a symlink}"
   299    local linkname="${3:?need a name to give the symlink}"
   300  
   301    if ! [ -d "$symdir" ]; then
   302      note "skipping symlink because directory does not exist: $symdir"
   303      return 1
   304    fi
   305    # ln(1) `-v` is busybox but is not POSIX
   306    if ! ln -sf -- "$target" "$symdir/$linkname"
   307    then
   308      note "failed to create a symlink in: $symdir"
   309      return 1
   310    fi
   311    ls -ld -- "$symdir/$linkname"
   312    if dir_is_in_PATH "$symdir"; then
   313      note "Symlink dir '$symdir' is already in your PATH"
   314      return 0
   315    fi
   316    note "Symlink dir '$symdir' is not in your PATH?"
   317    return 1
   318  }
   319  
   320  link_and_show_instructions() {
   321    local new_cmd="${1:?need a command which has been installed}"
   322  
   323    local target="$opt_install_dir/$new_cmd"
   324  
   325    echo
   326    note "NSC: $target"
   327    ls -ld -- "$target"
   328  
   329    if [ -n "$opt_symlink_dir" ]; then
   330      if maybe_make_symlink "$target" "$opt_symlink_dir" "$new_cmd"
   331      then
   332        return 0
   333      fi
   334    fi
   335  
   336    echo
   337    printf 'Now manually add %s to your $PATH\n' "$opt_install_dir"
   338  
   339    case "$(normalized_ostype)" in
   340      (windows) cat <<EOWINDOWS ;;
   341  Windows Cmd Prompt Example:
   342    setx path %path;"${opt_install_dir}"
   343  
   344  EOWINDOWS
   345  
   346      (*) cat <<EOOTHER ;;
   347  Bash Example:
   348    echo 'export PATH="\${PATH}:${opt_install_dir}"' >> ~/.bashrc
   349    source ~/.bashrc
   350  
   351  Zsh Example:
   352    echo 'path+=("${opt_install_dir}")' >> ~/.zshrc
   353    source ~/.zshrc
   354  
   355  EOOTHER
   356  
   357    esac
   358  }
   359  
   360  main "$@"