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 "$@"