oven-sh / bun

Incredibly fast JavaScript runtime, bundler, test runner, and package manager – all in one
https://bun.sh
Other
73.11k stars 2.67k forks source link

Install script should use POSIX and not bash #1936

Open coolaj86 opened 1 year ago

coolaj86 commented 1 year ago

What is the problem this feature would solve?

Bun would be universally installable on any POSIX system for which a binary exists.

Well... actually would need a POSIX .tar as well, but... one thing at a time. :) \ (and although .gz isn't POSIX, it seems to be more widely adopted than POSIX .Z)

Context: I'm adding bun to Webi (https://beta.webinstall.dev/bun). We do POSIX because over the years enough people have complained about bash scripts breaking stuff.

What is the feature you are proposing to solve the problem?

Running shellcheck and shfmt in POSIX mode (i.e. change the shebange to #!/bin/sh on install.sh

What alternatives have you considered?

N/A

P.S. I'm drafting this up right now. I don't know where install.sh lives, but I'll post below in probably 10 minutes or so.

P.P.S. Taking a little longer than I thought - there's A LOT of fairly complex bashisms here.

P.P.P.S. There's very inconsistent use of $x vs ${x} vs "$x" vs "${x}", so I'm curious to whether or not directories with spaces in their names are currently allowed.

Where is install.sh?

I didn't see a repo that seemed like a good place to do a PR.

PR Process

Trying to look at all of these changes in aggregate is pretty much impossible, but I can break them down into feature-by-feature commits such that each checkmarked change is very easy to digest and understand its correctness.

(this, of course, is just a Works On My Machine™ Proof-of-Concept)

coolaj86 commented 1 year ago

POSIX-compliant install

Changes

Works On My Machine™ Proof-of-Concept

(actual diff below)

#!/bin/sh
#shellcheck disable=SC2059
set -e
set -u

if [ "${OS:-}" = Windows_NT ]; then
    echo 'error: Please install bun using Windows Subsystem for Linux'
    exit 1
fi

# Reset
Color_Off=''

# Regular Colors
Red=''
Green=''
Dim='' # White

# Bold
Bold_White=''
Bold_Green=''

if [ -t 1 ]; then
    # Reset
    Color_Off='\033[0m' # Text Reset

    # Regular Colors
    Red='\033[0;31m'   # Red
    Green='\033[0;32m' # Green
    Dim='\033[0;2m'    # White

    # Bold
    Bold_Green='\033[1;32m' # Bold Green
    Bold_White='\033[1m'    # Bold White
fi

error() {
    printf "${Red}error${Color_Off}: $*\n" >&2
    exit 1
}

info() {
    printf "${Dim}$* ${Color_Off}\n"
}

info_bold() {
    printf "${Bold_White}$* ${Color_Off}\n"
}

success() {
    printf "${Green}$* ${Color_Off}\n"
}

if ! command -v unzip > /dev/null; then
    error 'unzip is required to install bun (see: https://github.com/oven-sh/bun#unzip-is-required)'
fi

if [ "$#" -gt 2 ]; then
    error 'Too many arguments, only 2 are allowed. The first can be a specific tag of bun to install. (e.g. "bun-v0.1.4") The second can be a build variant of bun to install. (e.g. "debug-info")'
fi

case $(uname -ms) in
'Darwin x86_64')
    target=darwin-x64
    ;;
'Darwin arm64')
    target=darwin-aarch64
    ;;
'Linux aarch64' | 'Linux arm64')
    target=linux-aarch64
    ;;
'Linux x86_64' | *)
    target=linux-x64
    ;;
esac

if [ "$target" = darwin-x64 ]; then
    # Is this process running in Rosetta?
    # redirect stderr to devnull to avoid error message when not running in Rosetta
    if [ "$(sysctl -n sysctl.proc_translated 2> /dev/null)" = 1 ]; then
        target=darwin-aarch64
        info "Your shell is running in Rosetta 2. Downloading bun for $target instead"
    fi
fi

GITHUB=${GITHUB-"https://github.com"}

github_repo="$GITHUB/oven-sh/bun"

if [ "$target" = darwin-x64 ]; then
    # If AVX2 isn't supported, use the -baseline build
    if [ "$(sysctl -a | grep machdep.cpu | grep AVX2)" = '' ]; then
        target=darwin-x64-baseline
    fi
fi

if [ $target = linux-x64 ]; then
    # If AVX2 isn't supported, use the -baseline build
    if ! grep -q avx2 /proc/cpuinfo; then
        target=linux-x64-baseline
    fi
fi

exe_name=bun

if [ "$#" = 2 ] && [ "$2" = debug-info ]; then
    target=$target-profile
    exe_name=bun-profile
    info "You requested a debug build of bun. More infomation will be shown if a crash occurs."
fi

if [ "$#" = 0 ]; then
    bun_uri=$github_repo/releases/latest/download/bun-$target.zip
else
    bun_uri=$github_repo/releases/download/$1/bun-$target.zip
fi

#TODO I don't understand why there was a layer of indirection here
install_env=BUN_INSTALL

install_dir="${BUN_INSTALL:-"${HOME}/.bun"}"
bin_dir="${install_dir}/bin"
bin_env="$(basename "${bin_dir}")"
exe="${bin_dir}/bun"

if [ ! -d "$bin_dir" ]; then
    mkdir -p "$bin_dir" ||
        error "Failed to create install directory \"$bin_dir\""
fi

curl --fail --location --progress-bar --output "$exe.zip" "$bun_uri" ||
    error "Failed to download bun from \"$bun_uri\""

unzip -oqd "$bin_dir" "$exe.zip" ||
    error 'Failed to extract bun'

mv "$bin_dir/bun-$target/$exe_name" "$exe" ||
    error 'Failed to move extracted bun to destination'

chmod +x "$exe" ||
    error 'Failed to set permissions on bun executable'

rm -r "$bin_dir/bun-$target" "$exe.zip"

tildify() {
    case ${1} in
    "$HOME/"*)
        echo "${1}" | sed "s:$HOME/:~/:"
        ;;
    *)
        echo "$1"
        ;;
    esac
}

success "bun was installed successfully to $Bold_Green$(tildify "$exe")"

# TODO not sure if this is correct. I'm not familiar with || :
if command -v bun > /dev/null; then
    # Install completions, but we don't care if it fails
    IS_BUN_AUTO_UPDATE=true $exe completions > /dev/null 2>&1 || :

    echo "Run 'bun --help' to get started"
    exit
fi

refresh_command=''

tilde_bin_dir="$(tildify "$bin_dir")"
quoted_install_dir="$(printf "%s" "$tilde_bin_dir" | sed 's:":\\":')"

# TODO not sure if I understood the intent here
case $quoted_install_dir in
"\"$HOME"/*)
    quoted_install_dir=$(printf "%s" "${quoted_install_dir}" | sed 's:/$::')
    ;;
*) ;;

esac

echo

case $(basename "$SHELL") in
fish)
    # Install completions, but we don't care if it fails
    IS_BUN_AUTO_UPDATE=true SHELL=fish $exe completions > /dev/null 2>&1 || :

    export_install_env="set --export $install_env $quoted_install_dir"
    export_path="set --export PATH $bin_env \$PATH"

    fish_config=$HOME/.config/fish/config.fish
    tilde_fish_config=$(tildify "$fish_config")

    if [ -w "$fish_config" ]; then
        {
            printf '\n# bun'

            echo "$export_install_env"
            echo "$export_path"
        } >> "$fish_config"

        info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_fish_config\""

        refresh_command="source $tilde_fish_config"
    else
        echo "Manually add the directory to $tilde_fish_config (or similar):"

        info_bold "  $export_install_env"
        info_bold "  $export_path"
    fi
    ;;
zsh)
    # Install completions, but we don't care if it fails
    IS_BUN_AUTO_UPDATE=true SHELL=zsh $exe completions > /dev/null 2>&1 || true

    export_install_env="export $install_env=$quoted_install_dir"
    export_path="export PATH=\"$bin_env:\$PATH\""

    zsh_config=$HOME/.zshrc
    tilde_zsh_config=$(tildify "$zsh_config")

    if [ -w "$zsh_config" ]; then
        {
            printf '\n# bun'

            echo "$export_install_env"
            echo "$export_path"
        } >> "$zsh_config"

        info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_zsh_config\""

        refresh_command="exec $SHELL"
    else
        echo "Manually add the directory to $tilde_zsh_config (or similar):"

        info_bold "  $export_install_env"
        info_bold "  $export_path"
    fi
    ;;
bash)
    export_install_env="export $install_env=$quoted_install_dir"
    export_path="export PATH=$bin_env:\$PATH"

    bash_configs="$HOME/.bashrc"
    bash_configs="${bash_configs}\n$HOME/.bash_profile"

    if [ -n "${XDG_CONFIG_HOME:-}" ]; then
        bash_configs="${bash_configs}\n$XDG_CONFIG_HOME/.bash_profile"
        bash_configs="${bash_configs}\n$XDG_CONFIG_HOME/.bashrc"
        bash_configs="${bash_configs}\n$XDG_CONFIG_HOME/bash_profile"
        bash_configs="${bash_configs}\n$XDG_CONFIG_HOME/bashrc"
    fi

    set_manually=true
    my_ifs="${IFS}"
    IFS='
'
    for bash_config in ${bash_configs}; do
        tilde_bash_config=$(tildify "$bash_config")

        if [ -w "$bash_config" ]; then
            {
                printf '\n# bun'

                echo "$export_install_env"
                echo "$export_path"
            } >> "$bash_config"

            info "Added \"$tilde_bin_dir\" to \$PATH in \"$tilde_bash_config\""

            refresh_command="source $bash_config"
            set_manually=false
            break
        fi
    done
    IFS="${my_ifs}"

    if [ "$set_manually" = true ]; then
        tilde_bash_config=$(tildify "$HOME/.bashrc")
        echo "Manually add the directory to $tilde_bash_config (or similar):"

        info_bold "  $export_install_env"
        info_bold "  $export_path"
    fi
    ;;
*)
    echo 'Manually add the directory to ~/.bashrc (or similar):'
    info_bold "  export $install_env=$quoted_install_dir"
    info_bold "  export PATH=\"$bin_env:\$PATH\""
    ;;
esac

echo
info "To get started, run:"
echo

if [ -n "$refresh_command" ]; then
    info_bold " $refresh_command"
fi

info_bold "  bun --help"

Diff Patch

1,2c1,4
< #!/usr/bin/env bash
< set -euo pipefail
---
> #!/bin/sh
> #shellcheck disable=SC2059
> set -e
> set -u
4c6
< if [[ ${OS:-} = Windows_NT ]]; then
---
> if [ "${OS:-}" = Windows_NT ]; then
21c23
< if [[ -t 1 ]]; then
---
> if [ -t 1 ]; then
36c38
<     echo -e "${Red}error${Color_Off}:" "$@" >&2
---
>     printf "${Red}error${Color_Off}: $*\n" >&2
41c43
<     echo -e "${Dim}$@ ${Color_Off}"
---
>     printf "${Dim}$* ${Color_Off}\n"
45c47
<     echo -e "${Bold_White}$@ ${Color_Off}"
---
>     printf "${Bold_White}$* ${Color_Off}\n"
49c51
<     echo -e "${Green}$@ ${Color_Off}"
---
>     printf "${Green}$* ${Color_Off}\n"
52c54
< command -v unzip >/dev/null ||
---
> if ! command -v unzip > /dev/null; then
53a56
> fi
55c58
< if [[ $# -gt 2 ]]; then
---
> if [ "$#" -gt 2 ]; then
74c77
< if [[ $target = darwin-x64 ]]; then
---
> if [ "$target" = darwin-x64 ]; then
77c80
<     if [[ $(sysctl -n sysctl.proc_translated 2>/dev/null) = 1 ]]; then
---
>     if [ "$(sysctl -n sysctl.proc_translated 2> /dev/null)" = 1 ]; then
87c90
< if [[ $target = darwin-x64 ]]; then
---
> if [ "$target" = darwin-x64 ]; then
89c92
<     if [[ $(sysctl -a | grep machdep.cpu | grep AVX2) == '' ]]; then
---
>     if [ "$(sysctl -a | grep machdep.cpu | grep AVX2)" = '' ]; then
94c97
< if [[ $target = linux-x64 ]]; then
---
> if [ $target = linux-x64 ]; then
96c99
<     if [[ $(cat /proc/cpuinfo | grep avx2) = '' ]]; then
---
>     if ! grep -q avx2 /proc/cpuinfo; then
103c106
< if [[ $# = 2 && $2 = debug-info ]]; then
---
> if [ "$#" = 2 ] && [ "$2" = debug-info ]; then
109c112
< if [[ $# = 0 ]]; then
---
> if [ "$#" = 0 ]; then
114a118
> #TODO I don't understand why there was a layer of indirection here
116d119
< bin_env=\$$install_env/bin
118,120c121,124
< install_dir=${!install_env:-$HOME/.bun}
< bin_dir=$install_dir/bin
< exe=$bin_dir/bun
---
> install_dir="${BUN_INSTALL:-"${HOME}/.bun"}"
> bin_dir="${install_dir}/bin"
> bin_env="$(basename "${bin_dir}")"
> exe="${bin_dir}/bun"
122c126
< if [[ ! -d $bin_dir ]]; then
---
> if [ ! -d "$bin_dir" ]; then
142,146c146,150
<     if [[ $1 = $HOME/* ]]; then
<         local replacement=\~/
<
<         echo "${1/$HOME\//$replacement}"
<     else
---
>     case ${1} in
>     "$HOME/"*)
>         echo "${1}" | sed "s:$HOME/:~/:"
>         ;;
>     *)
148c152,153
<     fi
---
>         ;;
>     esac
153c158,159
< if command -v bun >/dev/null; then
---
> # TODO not sure if this is correct. I'm not familiar with || :
> if command -v bun > /dev/null; then
155c161
<     IS_BUN_AUTO_UPDATE=true $exe completions &>/dev/null || :
---
>     IS_BUN_AUTO_UPDATE=true $exe completions > /dev/null 2>&1 || :
163,164c169,170
< tilde_bin_dir=$(tildify "$bin_dir")
< quoted_install_dir=\"${install_dir//\"/\\\"}\"
---
> tilde_bin_dir="$(tildify "$bin_dir")"
> quoted_install_dir="$(printf "%s" "$tilde_bin_dir" | sed 's:":\\":')"
166,168c172,177
< if [[ $quoted_install_dir = \"$HOME/* ]]; then
<     quoted_install_dir=${quoted_install_dir/$HOME\//\$HOME/}
< fi
---
> # TODO not sure if I understood the intent here
> case $quoted_install_dir in
> "\"$HOME"/*)
>     quoted_install_dir=$(printf "%s" "${quoted_install_dir}" | sed 's:/$::')
>     ;;
> *) ;;
169a179,180
> esac
>
175c186
<     IS_BUN_AUTO_UPDATE=true SHELL=fish $exe completions &>/dev/null || :
---
>     IS_BUN_AUTO_UPDATE=true SHELL=fish $exe completions > /dev/null 2>&1 || :
177,180c188,189
<     commands=(
<         "set --export $install_env $quoted_install_dir"
<         "set --export PATH $bin_env \$PATH"
<     )
---
>     export_install_env="set --export $install_env $quoted_install_dir"
>     export_path="set --export PATH $bin_env \$PATH"
185c194
<     if [[ -w $fish_config ]]; then
---
>     if [ -w "$fish_config" ]; then
187c196
<             echo -e '\n# bun'
---
>             printf '\n# bun'
189,192c198,200
<             for command in "${commands[@]}"; do
<                 echo "$command"
<             done
<         } >>"$fish_config"
---
>             echo "$export_install_env"
>             echo "$export_path"
>         } >> "$fish_config"
200,202c208,209
<         for command in "${commands[@]}"; do
<             info_bold "  $command"
<         done
---
>         info_bold "  $export_install_env"
>         info_bold "  $export_path"
207c214
<     IS_BUN_AUTO_UPDATE=true SHELL=zsh $exe completions &>/dev/null || :
---
>     IS_BUN_AUTO_UPDATE=true SHELL=zsh $exe completions > /dev/null 2>&1 || true
209,212c216,217
<     commands=(
<         "export $install_env=$quoted_install_dir"
<         "export PATH=\"$bin_env:\$PATH\""
<     )
---
>     export_install_env="export $install_env=$quoted_install_dir"
>     export_path="export PATH=\"$bin_env:\$PATH\""
217c222
<     if [[ -w $zsh_config ]]; then
---
>     if [ -w "$zsh_config" ]; then
219c224
<             echo -e '\n# bun'
---
>             printf '\n# bun'
221,224c226,228
<             for command in "${commands[@]}"; do
<                 echo "$command"
<             done
<         } >>"$zsh_config"
---
>             echo "$export_install_env"
>             echo "$export_path"
>         } >> "$zsh_config"
232,234c236,237
<         for command in "${commands[@]}"; do
<             info_bold "  $command"
<         done
---
>         info_bold "  $export_install_env"
>         info_bold "  $export_path"
238,241c241,242
<     commands=(
<         "export $install_env=$quoted_install_dir"
<         "export PATH=$bin_env:\$PATH"
<     )
---
>     export_install_env="export $install_env=$quoted_install_dir"
>     export_path="export PATH=$bin_env:\$PATH"
243,246c244,245
<     bash_configs=(
<         "$HOME/.bashrc"
<         "$HOME/.bash_profile"
<     )
---
>     bash_configs="$HOME/.bashrc"
>     bash_configs="${bash_configs}\n$HOME/.bash_profile"
248,254c247,251
<     if [[ ${XDG_CONFIG_HOME:-} ]]; then
<         bash_configs+=(
<             "$XDG_CONFIG_HOME/.bash_profile"
<             "$XDG_CONFIG_HOME/.bashrc"
<             "$XDG_CONFIG_HOME/bash_profile"
<             "$XDG_CONFIG_HOME/bashrc"
<         )
---
>     if [ -n "${XDG_CONFIG_HOME:-}" ]; then
>         bash_configs="${bash_configs}\n$XDG_CONFIG_HOME/.bash_profile"
>         bash_configs="${bash_configs}\n$XDG_CONFIG_HOME/.bashrc"
>         bash_configs="${bash_configs}\n$XDG_CONFIG_HOME/bash_profile"
>         bash_configs="${bash_configs}\n$XDG_CONFIG_HOME/bashrc"
258c255,258
<     for bash_config in "${bash_configs[@]}"; do
---
>     my_ifs="${IFS}"
>     IFS='
> '
>     for bash_config in ${bash_configs}; do
261c261
<         if [[ -w $bash_config ]]; then
---
>         if [ -w "$bash_config" ]; then
263c263
<                 echo -e '\n# bun'
---
>                 printf '\n# bun'
265,268c265,267
<                 for command in "${commands[@]}"; do
<                     echo "$command"
<                 done
<             } >>"$bash_config"
---
>                 echo "$export_install_env"
>                 echo "$export_path"
>             } >> "$bash_config"
276a276
>     IFS="${my_ifs}"
278c278,279
<     if [[ $set_manually = true ]]; then
---
>     if [ "$set_manually" = true ]; then
>         tilde_bash_config=$(tildify "$HOME/.bashrc")
281,283c282,283
<         for command in "${commands[@]}"; do
<             info_bold "  $command"
<         done
---
>         info_bold "  $export_install_env"
>         info_bold "  $export_path"
297c297
< if [[ $refresh_command ]]; then
---
> if [ -n "$refresh_command" ]; then
Electroid commented 1 year ago

Thanks so much for tinkering on this! Indeed we need to make the install script work on more platforms. Appreciate the patch, will take a look at getting this merged soon.

coolaj86 commented 1 year ago

@Electroid there are a few parts with bashisms that I didn't understand (and due to syntax are hard to search for).

I put TODO on those. If there's any question about how to translate that more correctly I'm happy to help.

JacksonKearl commented 1 year ago

FWIW, : is a POSIX-specificed alternative way of saying true (the || : phrase is just "possibly erroneous thing or true"). So the current translation provided by @coolaj86 should be fine. https://stackoverflow.com/questions/3224878/what-is-the-purpose-of-the-colon-gnu-bash-builtin

paperdave commented 7 months ago

is there a reason this proof of concept script was never opened as a PR?

coolaj86 commented 3 weeks ago

@paperdave I never got back around to it. Life. 🤷‍♂️

It's too bad GitHub doesn't have a "Weekend TODOs" list to pick from when I'm burnt out from regular work and need some fun time.

I'd be happy to do this next time I have some downtime to dedicate to it.