|
| 1 | +#!/usr/bin/env bash |
| 2 | +# Based on: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md |
| 3 | + |
| 4 | + |
| 5 | +INSTALL_ZSH=${1:-"true"} |
| 6 | +USERNAME=${2:-"automatic"} |
| 7 | +USER_UID=${3:-"automatic"} |
| 8 | +USER_GID=${4:-"automatic"} |
| 9 | +INSTALL_OH_MYS=${5:-"true"} |
| 10 | + |
| 11 | +set -e |
| 12 | + |
| 13 | +if [ "$(id -u)" -ne 0 ]; then |
| 14 | + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' |
| 15 | + exit 1 |
| 16 | +fi |
| 17 | + |
| 18 | +# Ensure that login shells get the correct path if the user updated the PATH using ENV. |
| 19 | +rm -f /etc/profile.d/00-restore-env.sh |
| 20 | +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh |
| 21 | +chmod +x /etc/profile.d/00-restore-env.sh |
| 22 | + |
| 23 | +# If in automatic mode, determine if a user already exists, if not use vscode |
| 24 | +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then |
| 25 | + USERNAME="" |
| 26 | + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") |
| 27 | + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do |
| 28 | + if id -u ${CURRENT_USER} > /dev/null 2>&1; then |
| 29 | + USERNAME=${CURRENT_USER} |
| 30 | + break |
| 31 | + fi |
| 32 | + done |
| 33 | + if [ "${USERNAME}" = "" ]; then |
| 34 | + USERNAME=vscode |
| 35 | + fi |
| 36 | +elif [ "${USERNAME}" = "none" ]; then |
| 37 | + USERNAME=root |
| 38 | + USER_UID=0 |
| 39 | + USER_GID=0 |
| 40 | +fi |
| 41 | + |
| 42 | +# Create or update a non-root user to match UID/GID. |
| 43 | +if id -u ${USERNAME} > /dev/null 2>&1; then |
| 44 | + # User exists, update if needed |
| 45 | + if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -G $USERNAME)" ]; then |
| 46 | + groupmod --gid $USER_GID $USERNAME |
| 47 | + usermod --gid $USER_GID $USERNAME |
| 48 | + fi |
| 49 | + if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then |
| 50 | + usermod --uid $USER_UID $USERNAME |
| 51 | + fi |
| 52 | +else |
| 53 | + # Create user |
| 54 | + if [ "${USER_GID}" = "automatic" ]; then |
| 55 | + groupadd $USERNAME |
| 56 | + else |
| 57 | + groupadd --gid $USER_GID $USERNAME |
| 58 | + fi |
| 59 | + if [ "${USER_UID}" = "automatic" ]; then |
| 60 | + useradd -s /bin/bash --gid $USERNAME -m $USERNAME |
| 61 | + else |
| 62 | + useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME |
| 63 | + fi |
| 64 | +fi |
| 65 | + |
| 66 | +# Add add sudo support for non-root user |
| 67 | +if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then |
| 68 | + mkdir -p /etc/sudoers.d |
| 69 | + echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME |
| 70 | + chmod 0440 /etc/sudoers.d/$USERNAME |
| 71 | + EXISTING_NON_ROOT_USER="${USERNAME}" |
| 72 | +fi |
| 73 | + |
| 74 | +# ** Shell customization section ** |
| 75 | +if [ "${USERNAME}" = "root" ]; then |
| 76 | + USER_RC_PATH="/root" |
| 77 | +else |
| 78 | + USER_RC_PATH="/home/${USERNAME}" |
| 79 | +fi |
| 80 | + |
| 81 | +# .bashrc/.zshrc snippet |
| 82 | +RC_SNIPPET="$(cat << 'EOF' |
| 83 | +
|
| 84 | +if [ -z "${USER}" ]; then export USER=$(whoami); fi |
| 85 | +if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi |
| 86 | +
|
| 87 | +# Display optional first run image specific notice if configured and terminal is interactive |
| 88 | +if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then |
| 89 | + if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then |
| 90 | + cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" |
| 91 | + elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then |
| 92 | + cat "/workspaces/.codespaces/shared/first-run-notice.txt" |
| 93 | + fi |
| 94 | + mkdir -p "$HOME/.config/vscode-dev-containers" |
| 95 | + # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it |
| 96 | + ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) |
| 97 | +fi |
| 98 | +
|
| 99 | +EOF |
| 100 | +)" |
| 101 | + |
| 102 | +# code shim, it fallbacks to code-insiders if code is not available |
| 103 | +cat << 'EOF' > /usr/local/bin/code |
| 104 | +#!/bin/sh |
| 105 | +
|
| 106 | +get_in_path_except_current() { |
| 107 | + which -a "$1" | grep -A1 "$0" | grep -v "$0" |
| 108 | +} |
| 109 | +
|
| 110 | +code="$(get_in_path_except_current code)" |
| 111 | +
|
| 112 | +if [ -n "$code" ]; then |
| 113 | + exec "$code" "$@" |
| 114 | +elif [ "$(command -v code-insiders)" ]; then |
| 115 | + exec code-insiders "$@" |
| 116 | +else |
| 117 | + echo "code or code-insiders is not installed" >&2 |
| 118 | + exit 127 |
| 119 | +fi |
| 120 | +EOF |
| 121 | +chmod +x /usr/local/bin/code |
| 122 | + |
| 123 | +# systemctl shim - tells people to use 'service' if systemd is not running |
| 124 | +cat << 'EOF' > /usr/local/bin/systemctl |
| 125 | +#!/bin/sh |
| 126 | +set -e |
| 127 | +if [ -d "/run/systemd/system" ]; then |
| 128 | + exec /bin/systemctl/systemctl "$@" |
| 129 | +else |
| 130 | + echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services intead. e.g.: \n\nservice --status-all' |
| 131 | +fi |
| 132 | +EOF |
| 133 | +chmod +x /usr/local/bin/systemctl |
| 134 | + |
| 135 | +# Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme |
| 136 | +CODESPACES_BASH="$(cat \ |
| 137 | +<<'EOF' |
| 138 | +
|
| 139 | +# Codespaces bash prompt theme |
| 140 | +__bash_prompt() { |
| 141 | + local userpart='`export XIT=$? \ |
| 142 | + && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ |
| 143 | + && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' |
| 144 | + local gitbranch='`\ |
| 145 | + export BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null); \ |
| 146 | + if [ "${BRANCH}" = "HEAD" ]; then \ |
| 147 | + export BRANCH=$(git describe --contains --all HEAD 2>/dev/null); \ |
| 148 | + fi; \ |
| 149 | + if [ "${BRANCH}" != "" ]; then \ |
| 150 | + echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ |
| 151 | + && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ |
| 152 | + echo -n " \[\033[1;33m\]✗"; \ |
| 153 | + fi \ |
| 154 | + && echo -n "\[\033[0;36m\]) "; \ |
| 155 | + fi`' |
| 156 | + local lightblue='\[\033[1;34m\]' |
| 157 | + local removecolor='\[\033[0m\]' |
| 158 | + PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " |
| 159 | + unset -f __bash_prompt |
| 160 | +} |
| 161 | +__bash_prompt |
| 162 | +
|
| 163 | +EOF |
| 164 | +)" |
| 165 | +CODESPACES_ZSH="$(cat \ |
| 166 | +<<'EOF' |
| 167 | +__zsh_prompt() { |
| 168 | + local prompt_username |
| 169 | + if [ ! -z "${GITHUB_USER}" ]; then |
| 170 | + prompt_username="@${GITHUB_USER}" |
| 171 | + else |
| 172 | + prompt_username="%n" |
| 173 | + fi |
| 174 | + PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow |
| 175 | + PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd |
| 176 | + PROMPT+='$(git_prompt_info)%{$fg[white]%}$ %{$reset_color%}' # Git status |
| 177 | + unset -f __zsh_prompt |
| 178 | +} |
| 179 | +ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" |
| 180 | +ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " |
| 181 | +ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" |
| 182 | +ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" |
| 183 | +__zsh_prompt |
| 184 | +EOF |
| 185 | +)" |
| 186 | + |
| 187 | +# Add notice that Oh My Bash! has been removed from images and how to provide information on how to install manually |
| 188 | +OMB_README="$(cat \ |
| 189 | +<<'EOF' |
| 190 | +"Oh My Bash!" has been removed from this image in favor of a simple shell prompt. If you |
| 191 | +still wish to use it, remove "~/.oh-my-bash" and install it from: https://github.com/ohmybash/oh-my-bash |
| 192 | +You may also want to consider "Bash-it" as an alternative: https://github.com/bash-it/bash-it |
| 193 | +See here for infomation on adding it to your image or dotfiles: https://aka.ms/codespaces/omb-remove |
| 194 | +EOF |
| 195 | +)" |
| 196 | +OMB_STUB="$(cat \ |
| 197 | +<<'EOF' |
| 198 | +#!/usr/bin/env bash |
| 199 | +if [ -t 1 ]; then |
| 200 | + cat $HOME/.oh-my-bash/README.md |
| 201 | +fi |
| 202 | +EOF |
| 203 | +)" |
| 204 | + |
| 205 | +# Add RC snippet and custom bash prompt |
| 206 | +if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then |
| 207 | + echo "${RC_SNIPPET}" >> /etc/bash.bashrc |
| 208 | + echo "${CODESPACES_BASH}" >> "${USER_RC_PATH}/.bashrc" |
| 209 | + echo 'export PROMPT_DIRTRIM=4' >> "${USER_RC_PATH}/.bashrc" |
| 210 | + if [ "${USERNAME}" != "root" ]; then |
| 211 | + echo "${CODESPACES_BASH}" >> "/root/.bashrc" |
| 212 | + echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" |
| 213 | + fi |
| 214 | + chown ${USERNAME}:${USERNAME} "${USER_RC_PATH}/.bashrc" |
| 215 | + RC_SNIPPET_ALREADY_ADDED="true" |
| 216 | +fi |
| 217 | + |
| 218 | +# Add stub for Oh My Bash! |
| 219 | +if [ ! -d "${USER_RC_PATH}/.oh-my-bash}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then |
| 220 | + mkdir -p "${USER_RC_PATH}/.oh-my-bash" "/root/.oh-my-bash" |
| 221 | + echo "${OMB_README}" >> "${USER_RC_PATH}/.oh-my-bash/README.md" |
| 222 | + echo "${OMB_STUB}" >> "${USER_RC_PATH}/.oh-my-bash/oh-my-bash.sh" |
| 223 | + chmod +x "${USER_RC_PATH}/.oh-my-bash/oh-my-bash.sh" |
| 224 | + if [ "${USERNAME}" != "root" ]; then |
| 225 | + echo "${OMB_README}" >> "/root/.oh-my-bash/README.md" |
| 226 | + echo "${OMB_STUB}" >> "/root/.oh-my-bash/oh-my-bash.sh" |
| 227 | + chmod +x "/root/.oh-my-bash/oh-my-bash.sh" |
| 228 | + fi |
| 229 | + chown -R "${USERNAME}:${USERNAME}" "${USER_RC_PATH}/.oh-my-bash" |
| 230 | +fi |
| 231 | + |
| 232 | +# Optionally install and configure zsh and Oh My Zsh! |
| 233 | +if [ "${INSTALL_ZSH}" = "true" ]; then |
| 234 | + if ! type zsh > /dev/null 2>&1; then |
| 235 | + apt-get install -y zsh |
| 236 | + fi |
| 237 | + if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then |
| 238 | + echo "${RC_SNIPPET}" >> /etc/zsh/zshrc |
| 239 | + ZSH_ALREADY_INSTALLED="true" |
| 240 | + fi |
| 241 | + |
| 242 | + # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. |
| 243 | + # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. |
| 244 | + OH_MY_INSTALL_DIR="${USER_RC_PATH}/.oh-my-zsh" |
| 245 | + if [ ! -d "${OH_MY_INSTALL_DIR}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then |
| 246 | + TEMPLATE_PATH="${OH_MY_INSTALL_DIR}/templates/zshrc.zsh-template" |
| 247 | + USER_RC_FILE="${USER_RC_PATH}/.zshrc" |
| 248 | + umask g-w,o-w |
| 249 | + mkdir -p ${OH_MY_INSTALL_DIR} |
| 250 | + git clone --depth=1 \ |
| 251 | + -c core.eol=lf \ |
| 252 | + -c core.autocrlf=false \ |
| 253 | + -c fsck.zeroPaddedFilemode=ignore \ |
| 254 | + -c fetch.fsck.zeroPaddedFilemode=ignore \ |
| 255 | + -c receive.fsck.zeroPaddedFilemode=ignore \ |
| 256 | + "https://github.com/ohmyzsh/ohmyzsh" "${OH_MY_INSTALL_DIR}" 2>&1 |
| 257 | + echo -e "$(cat "${TEMPLATE_PATH}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${USER_RC_FILE} |
| 258 | + sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${USER_RC_FILE} |
| 259 | + |
| 260 | + mkdir -p ${OH_MY_INSTALL_DIR}/custom/themes |
| 261 | + echo "${CODESPACES_ZSH}" > "${OH_MY_INSTALL_DIR}/custom/themes/codespaces.zsh-theme" |
| 262 | + # Shrink git while still enabling updates |
| 263 | + cd "${OH_MY_INSTALL_DIR}" |
| 264 | + git repack -a -d -f --depth=1 --window=1 |
| 265 | + # Copy to non-root user if one is specified |
| 266 | + if [ "${USERNAME}" != "root" ]; then |
| 267 | + cp -rf "${USER_RC_FILE}" "${OH_MY_INSTALL_DIR}" /root |
| 268 | + chown -R ${USERNAME}:${USERNAME} "${USER_RC_PATH}" |
| 269 | + fi |
| 270 | + fi |
| 271 | +fi |
| 272 | + |
| 273 | +# Persist image metadata info, script if meta.env found in same directory |
| 274 | +META_INFO_SCRIPT="$(cat << 'EOF' |
| 275 | +#!/bin/sh |
| 276 | +. /usr/local/etc/vscode-dev-containers/meta.env |
| 277 | +
|
| 278 | +# Minimal output |
| 279 | +if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then |
| 280 | + echo "${VERSION}" |
| 281 | + exit 0 |
| 282 | +elif [ "$1" = "release" ]; then |
| 283 | + echo "${GIT_REPOSITORY_RELEASE}" |
| 284 | + exit 0 |
| 285 | +elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then |
| 286 | + echo "${CONTENTS_URL}" |
| 287 | + exit 0 |
| 288 | +fi |
| 289 | +
|
| 290 | +#Full output |
| 291 | +echo |
| 292 | +echo "Development container image information" |
| 293 | +echo |
| 294 | +if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi |
| 295 | +if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi |
| 296 | +if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi |
| 297 | +if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi |
| 298 | +if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi |
| 299 | +if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi |
| 300 | +if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi |
| 301 | +echo |
| 302 | +EOF |
| 303 | +)" |
| 304 | +SCRIPT_DIR="$(cd $(dirname $0) && pwd)" |
| 305 | +if [ -f "${SCRIPT_DIR}/meta.env" ]; then |
| 306 | + mkdir -p /usr/local/etc/vscode-dev-containers/ |
| 307 | + cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env |
| 308 | + echo "${META_INFO_SCRIPT}" > /usr/local/bin/devcontainer-info |
| 309 | + chmod +x /usr/local/bin/devcontainer-info |
| 310 | +fi |
| 311 | + |
| 312 | +echo "Done!" |
0 commit comments