diff --git a/.github/ISSUE_TEMPLATE/install.yaml b/.github/ISSUE_TEMPLATE/install.yaml new file mode 100644 index 0000000000..c5fc63c99a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/install.yaml @@ -0,0 +1,42 @@ +name: "Install problems" +description: "If you have problems deploying InvenTree" +labels: ["question", "triage:not-checked", "setup"] +body: + - type: checkboxes + id: deployment + attributes: + label: "Deployment Method" + options: + - label: "Installer" + - label: "Docker Development" + - label: "Docker Production" + - label: "Bare metal Development" + - label: "Bare metal Production" + - type: textarea + id: description + validations: + required: true + attributes: + label: "Describe the problem*" + description: "A clear and concise description of what is failing." + - type: textarea + id: steps-to-reproduce + validations: + required: true + attributes: + label: "Steps to Reproduce" + description: "Steps to reproduce the behavior, please make it detailed" + placeholder: | + 0. Link to all docs you used + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + - type: textarea + id: logs + attributes: + label: "Relevant log output" + description: Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks. + render: bash + validations: + required: false diff --git a/README.md b/README.md index b69390151d..b24c814c8f 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ There are several options to deploy InvenTree. Single line install - read [the docs](https://inventree.readthedocs.io/en/latest/start/installer/) for supported distros and details about the function: ```bash -curl https://raw.githubusercontent.com/InvenTree/InvenTree/master/contrib/install.sh | sh +wget -Nq https://raw.githubusercontent.com/InvenTree/InvenTree/master/contrib/install.sh && bash install.sh ``` diff --git a/contrib/install.sh b/contrib/install.sh index ef222dee74..196d0381ec 100755 --- a/contrib/install.sh +++ b/contrib/install.sh @@ -1,54 +1,341 @@ -get_distribution() { - lsb_dist="" - # Every system that we officially support has /etc/os-release - if [ -r /etc/os-release ]; then - lsb_dist="$(. /etc/os-release && echo "$ID")" - fi - # Returning an empty string here should be alright since the - # case statements don't act unless you provide an actual value - echo "$lsb_dist" +#!/usr/bin/env bash +# This script was generated by bashly 0.8.9 (https://bashly.dannyb.co) +# Modifying it manually is not recommended + +# :wrapper.bash3_bouncer +if [[ "${BASH_VERSINFO:-0}" -lt 4 ]]; then + printf "bash version 4 or higher is required\n" >&2 + exit 1 +fi + +# :command.master_script +# :command.root_command +root_command() { + # src/root_command.sh + # Settings + source_url=${args[source]} + publisher=${args[publisher]} + # Flags + no_call=${args[--no-call]} + dry_run=${args[--dry-run]} + + REQS="wget apt-transport-https" + + function do_call() { + if [[ $dry_run ]]; then + echo -e "### DRY RUN: \n$1" + else + $1 + fi + } + + function get_distribution { + if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$NAME + VER=$VERSION_ID + elif type lsb_release >/dev/null 2>&1; then + OS=$(lsb_release -si) + VER=$(lsb_release -sr) + elif [ -f /etc/lsb-release ]; then + . /etc/lsb-release + OS=$DISTRIB_ID + VER=$DISTRIB_RELEASE + elif [ -f /etc/debian_version ]; then + OS=Debian + VER=$(cat /etc/debian_version) + elif [ -f /etc/SuSe-release ]; then + OS=SEL + elif [ -f /etc/redhat-release ]; then + OS=RedHat + else + OS=$(uname -s) + VER=$(uname -r) + fi + } + + echo "### Installer for InvenTree - source: $publisher/$source_url" + + # Check if os and version is supported + get_distribution + echo "### Detected distribution: $OS $VER" + NOT_SUPPORTED=false + case "$OS" in + Ubuntu) + if [[ $VER != "20.04" ]]; then + NOT_SUPPORTED=true + fi + ;; + Debian | Raspbian) + if [[ $VER != "11" ]]; then + NOT_SUPPORTED=true + fi + ;; + *) + echo "### Distribution not supported" + NOT_SUPPORTED=true + ;; + esac + + if [[ $NOT_SUPPORTED ]]; then + echo "This OS is currently not supported" + echo "please install manually using https://inventree.readthedocs.io/en/stable/start/install/" + echo "or check https://github.com/inventree/InvenTree/issues/3836 for packaging for your OS." + echo "If you think this is a bug please file an issue at" + echo "https://github.com/inventree/InvenTree/issues/new?template=install.yaml" + + exit 1 + fi + + echo "### Installing required packages for download" + for pkg in $REQS; do + if dpkg-query -W -f'${Status}' "$pkg" 2>/dev/null | grep -q "ok installed"; then + true + else + do_call "sudo apt-get -yqq install $pkg" + fi + done + + echo "### Adding key and package source" + # Add key + do_call "wget -qO- https://dl.packager.io/srv/$publisher/InvenTree/key | sudo apt-key add -" + # Add packagelist + do_call "sudo wget -O /etc/apt/sources.list.d/inventree.list https://dl.packager.io/srv/$publisher/InvenTree/$source_url/installer/${lsb_dist}/${dist_version}.repo" + + echo "### Updateing package lists" + do_call "sudo apt-get update" + + # Set up environment for install + echo "### Setting installer args" + if [[ $no_call ]]; then + do_call "export NO_CALL=true" + fi + + echo "### Installing InvenTree" + do_call "sudo apt-get install inventree -y" + + echo "### Install done!" + } -get_distribution -case "$lsb_dist" in -ubuntu) - if command_exists lsb_release; then - dist_version="$(lsb_release -r | cut -f2)" - fi - if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then - dist_version="$(. /etc/lsb-release && echo "$DISTRIB_RELEASE")" - fi - ;; -debian | raspbian) - dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')" - lsb_dist="debian" - ;; -centos | rhel | sles) - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - ;; -*) - if command_exists lsb_release; then - dist_version="$(lsb_release --release | cut -f2)" - fi - if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then - dist_version="$(. /etc/os-release && echo "$VERSION_ID")" - fi - ;; -esac -echo "### ${lsb_dist} ${dist_version} detected" +# :command.version_command +version_command() { + echo "$version" +} -# Make sure the depencies are there -sudo apt-get install wget apt-transport-https -y +# :command.usage +install_usage() { + if [[ -n $long_usage ]]; then + printf "install - Interactive installer for InvenTree\n" + echo -echo "### Add key and package source" -# Add key -wget -qO- https://dl.packager.io/srv/matmair/InvenTree/key | sudo apt-key add - -# Add packagelist -sudo wget -O /etc/apt/sources.list.d/inventree.list https://dl.packager.io/srv/matmair/InvenTree/deploy-test/installer/${lsb_dist}/${dist_version}.repo + else + printf "install - Interactive installer for InvenTree\n" + echo -echo "### Install InvenTree" -# Update repos and install inventree -sudo apt-get update -sudo apt-get install inventree -y + fi + + printf "Usage:\n" + printf " install [SOURCE] [PUBLISHER] [OPTIONS]\n" + printf " install --help | -h\n" + printf " install --version | -v\n" + echo + + # :command.long_usage + if [[ -n $long_usage ]]; then + printf "Options:\n" + + # :command.usage_fixed_flags + echo " --help, -h" + printf " Show this help\n" + echo + echo " --version, -v" + printf " Show version number\n" + echo + + # :command.usage_flags + # :flag.usage + echo " --no-call, -n" + printf " Do not call outside APIs (only functionally needed)\n" + echo + + # :flag.usage + echo " --dry-run, -d" + printf " Dry run (do not install anything)\n" + echo + + # :command.usage_args + printf "Arguments:\n" + + # :argument.usage + echo " SOURCE" + printf " Package source that should be used\n" + printf " Allowed: stable, master, main\n" + printf " Default: stable\n" + echo + + # :argument.usage + echo " PUBLISHER" + printf " Publisher that should be used\n" + printf " Default: inventree\n" + echo + + # :command.usage_examples + printf "Examples:\n" + printf " install\n" + printf " install master --no-call\n" + printf " install master matmair --dry-run\n" + echo + + fi +} + +# :command.normalize_input +normalize_input() { + local arg flags + + while [[ $# -gt 0 ]]; do + arg="$1" + if [[ $arg =~ ^(--[a-zA-Z0-9_\-]+)=(.+)$ ]]; then + input+=("${BASH_REMATCH[1]}") + input+=("${BASH_REMATCH[2]}") + elif [[ $arg =~ ^(-[a-zA-Z0-9])=(.+)$ ]]; then + input+=("${BASH_REMATCH[1]}") + input+=("${BASH_REMATCH[2]}") + elif [[ $arg =~ ^-([a-zA-Z0-9][a-zA-Z0-9]+)$ ]]; then + flags="${BASH_REMATCH[1]}" + for (( i=0 ; i < ${#flags} ; i++ )); do + input+=("-${flags:i:1}") + done + else + input+=("$arg") + fi + + shift + done +} +# :command.inspect_args +inspect_args() { + readarray -t sorted_keys < <(printf '%s\n' "${!args[@]}" | sort) + if (( ${#args[@]} )); then + echo args: + for k in "${sorted_keys[@]}"; do echo "- \${args[$k]} = ${args[$k]}"; done + else + echo args: none + fi + + if (( ${#other_args[@]} )); then + echo + echo other_args: + echo "- \${other_args[*]} = ${other_args[*]}" + for i in "${!other_args[@]}"; do + echo "- \${other_args[$i]} = ${other_args[$i]}" + done + fi +} + +# :command.command_functions + +# :command.parse_requirements +parse_requirements() { + # :command.fixed_flags_filter + case "${1:-}" in + --version | -v ) + version_command + exit + ;; + + --help | -h ) + long_usage=yes + install_usage + exit + ;; + + esac + + # :command.command_filter + action="root" + + # :command.parse_requirements_while + while [[ $# -gt 0 ]]; do + key="$1" + case "$key" in + # :flag.case + --no-call | -n ) + + # :flag.case_no_arg + args[--no-call]=1 + shift + ;; + + # :flag.case + --dry-run | -d ) + + # :flag.case_no_arg + args[--dry-run]=1 + shift + ;; + + -?* ) + printf "invalid option: %s\n" "$key" >&2 + exit 1 + ;; + + * ) + # :command.parse_requirements_case + # :command.parse_requirements_case_simple + if [[ -z ${args[source]+x} ]]; then + + args[source]=$1 + shift + elif [[ -z ${args[publisher]+x} ]]; then + + args[publisher]=$1 + shift + else + printf "invalid argument: %s\n" "$key" >&2 + exit 1 + fi + + ;; + + esac + done + + # :command.default_assignments + [[ -n ${args[source]:-} ]] || args[source]="stable" + [[ -n ${args[publisher]:-} ]] || args[publisher]="inventree" + + # :command.whitelist_filter + if [[ ! ${args[source]} =~ ^(stable|master|main)$ ]]; then + printf "%s\n" "source must be one of: stable, master, main" >&2 + exit 1 + fi + +} + +# :command.initialize +initialize() { + version="2.0" + long_usage='' + set -e + + # src/initialize.sh + +} + +# :command.run +run() { + declare -A args=() + declare -a other_args=() + declare -a input=() + normalize_input "$@" + parse_requirements "${input[@]}" + + if [[ $action == "root" ]]; then + root_command + fi +} + +initialize +run "$@" diff --git a/contrib/installer/README b/contrib/installer/README new file mode 100644 index 0000000000..78ac001a72 --- /dev/null +++ b/contrib/installer/README @@ -0,0 +1,14 @@ +The installer is generated using bashly. + +## Installation +Check out the docs: https://bashly.dannyb.co/installation/ + +If you have ruby already installed run +```bash +gem install bashly +``` + +## Regenerate script +```bash +bashly generate +``` diff --git a/contrib/installer/install b/contrib/installer/install new file mode 100755 index 0000000000..196d0381ec --- /dev/null +++ b/contrib/installer/install @@ -0,0 +1,341 @@ +#!/usr/bin/env bash +# This script was generated by bashly 0.8.9 (https://bashly.dannyb.co) +# Modifying it manually is not recommended + +# :wrapper.bash3_bouncer +if [[ "${BASH_VERSINFO:-0}" -lt 4 ]]; then + printf "bash version 4 or higher is required\n" >&2 + exit 1 +fi + +# :command.master_script +# :command.root_command +root_command() { + # src/root_command.sh + # Settings + source_url=${args[source]} + publisher=${args[publisher]} + # Flags + no_call=${args[--no-call]} + dry_run=${args[--dry-run]} + + REQS="wget apt-transport-https" + + function do_call() { + if [[ $dry_run ]]; then + echo -e "### DRY RUN: \n$1" + else + $1 + fi + } + + function get_distribution { + if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$NAME + VER=$VERSION_ID + elif type lsb_release >/dev/null 2>&1; then + OS=$(lsb_release -si) + VER=$(lsb_release -sr) + elif [ -f /etc/lsb-release ]; then + . /etc/lsb-release + OS=$DISTRIB_ID + VER=$DISTRIB_RELEASE + elif [ -f /etc/debian_version ]; then + OS=Debian + VER=$(cat /etc/debian_version) + elif [ -f /etc/SuSe-release ]; then + OS=SEL + elif [ -f /etc/redhat-release ]; then + OS=RedHat + else + OS=$(uname -s) + VER=$(uname -r) + fi + } + + echo "### Installer for InvenTree - source: $publisher/$source_url" + + # Check if os and version is supported + get_distribution + echo "### Detected distribution: $OS $VER" + NOT_SUPPORTED=false + case "$OS" in + Ubuntu) + if [[ $VER != "20.04" ]]; then + NOT_SUPPORTED=true + fi + ;; + Debian | Raspbian) + if [[ $VER != "11" ]]; then + NOT_SUPPORTED=true + fi + ;; + *) + echo "### Distribution not supported" + NOT_SUPPORTED=true + ;; + esac + + if [[ $NOT_SUPPORTED ]]; then + echo "This OS is currently not supported" + echo "please install manually using https://inventree.readthedocs.io/en/stable/start/install/" + echo "or check https://github.com/inventree/InvenTree/issues/3836 for packaging for your OS." + echo "If you think this is a bug please file an issue at" + echo "https://github.com/inventree/InvenTree/issues/new?template=install.yaml" + + exit 1 + fi + + echo "### Installing required packages for download" + for pkg in $REQS; do + if dpkg-query -W -f'${Status}' "$pkg" 2>/dev/null | grep -q "ok installed"; then + true + else + do_call "sudo apt-get -yqq install $pkg" + fi + done + + echo "### Adding key and package source" + # Add key + do_call "wget -qO- https://dl.packager.io/srv/$publisher/InvenTree/key | sudo apt-key add -" + # Add packagelist + do_call "sudo wget -O /etc/apt/sources.list.d/inventree.list https://dl.packager.io/srv/$publisher/InvenTree/$source_url/installer/${lsb_dist}/${dist_version}.repo" + + echo "### Updateing package lists" + do_call "sudo apt-get update" + + # Set up environment for install + echo "### Setting installer args" + if [[ $no_call ]]; then + do_call "export NO_CALL=true" + fi + + echo "### Installing InvenTree" + do_call "sudo apt-get install inventree -y" + + echo "### Install done!" + +} + +# :command.version_command +version_command() { + echo "$version" +} + +# :command.usage +install_usage() { + if [[ -n $long_usage ]]; then + printf "install - Interactive installer for InvenTree\n" + echo + + else + printf "install - Interactive installer for InvenTree\n" + echo + + fi + + printf "Usage:\n" + printf " install [SOURCE] [PUBLISHER] [OPTIONS]\n" + printf " install --help | -h\n" + printf " install --version | -v\n" + echo + + # :command.long_usage + if [[ -n $long_usage ]]; then + printf "Options:\n" + + # :command.usage_fixed_flags + echo " --help, -h" + printf " Show this help\n" + echo + echo " --version, -v" + printf " Show version number\n" + echo + + # :command.usage_flags + # :flag.usage + echo " --no-call, -n" + printf " Do not call outside APIs (only functionally needed)\n" + echo + + # :flag.usage + echo " --dry-run, -d" + printf " Dry run (do not install anything)\n" + echo + + # :command.usage_args + printf "Arguments:\n" + + # :argument.usage + echo " SOURCE" + printf " Package source that should be used\n" + printf " Allowed: stable, master, main\n" + printf " Default: stable\n" + echo + + # :argument.usage + echo " PUBLISHER" + printf " Publisher that should be used\n" + printf " Default: inventree\n" + echo + + # :command.usage_examples + printf "Examples:\n" + printf " install\n" + printf " install master --no-call\n" + printf " install master matmair --dry-run\n" + echo + + fi +} + +# :command.normalize_input +normalize_input() { + local arg flags + + while [[ $# -gt 0 ]]; do + arg="$1" + if [[ $arg =~ ^(--[a-zA-Z0-9_\-]+)=(.+)$ ]]; then + input+=("${BASH_REMATCH[1]}") + input+=("${BASH_REMATCH[2]}") + elif [[ $arg =~ ^(-[a-zA-Z0-9])=(.+)$ ]]; then + input+=("${BASH_REMATCH[1]}") + input+=("${BASH_REMATCH[2]}") + elif [[ $arg =~ ^-([a-zA-Z0-9][a-zA-Z0-9]+)$ ]]; then + flags="${BASH_REMATCH[1]}" + for (( i=0 ; i < ${#flags} ; i++ )); do + input+=("-${flags:i:1}") + done + else + input+=("$arg") + fi + + shift + done +} +# :command.inspect_args +inspect_args() { + readarray -t sorted_keys < <(printf '%s\n' "${!args[@]}" | sort) + if (( ${#args[@]} )); then + echo args: + for k in "${sorted_keys[@]}"; do echo "- \${args[$k]} = ${args[$k]}"; done + else + echo args: none + fi + + if (( ${#other_args[@]} )); then + echo + echo other_args: + echo "- \${other_args[*]} = ${other_args[*]}" + for i in "${!other_args[@]}"; do + echo "- \${other_args[$i]} = ${other_args[$i]}" + done + fi +} + +# :command.command_functions + +# :command.parse_requirements +parse_requirements() { + # :command.fixed_flags_filter + case "${1:-}" in + --version | -v ) + version_command + exit + ;; + + --help | -h ) + long_usage=yes + install_usage + exit + ;; + + esac + + # :command.command_filter + action="root" + + # :command.parse_requirements_while + while [[ $# -gt 0 ]]; do + key="$1" + case "$key" in + # :flag.case + --no-call | -n ) + + # :flag.case_no_arg + args[--no-call]=1 + shift + ;; + + # :flag.case + --dry-run | -d ) + + # :flag.case_no_arg + args[--dry-run]=1 + shift + ;; + + -?* ) + printf "invalid option: %s\n" "$key" >&2 + exit 1 + ;; + + * ) + # :command.parse_requirements_case + # :command.parse_requirements_case_simple + if [[ -z ${args[source]+x} ]]; then + + args[source]=$1 + shift + elif [[ -z ${args[publisher]+x} ]]; then + + args[publisher]=$1 + shift + else + printf "invalid argument: %s\n" "$key" >&2 + exit 1 + fi + + ;; + + esac + done + + # :command.default_assignments + [[ -n ${args[source]:-} ]] || args[source]="stable" + [[ -n ${args[publisher]:-} ]] || args[publisher]="inventree" + + # :command.whitelist_filter + if [[ ! ${args[source]} =~ ^(stable|master|main)$ ]]; then + printf "%s\n" "source must be one of: stable, master, main" >&2 + exit 1 + fi + +} + +# :command.initialize +initialize() { + version="2.0" + long_usage='' + set -e + + # src/initialize.sh + +} + +# :command.run +run() { + declare -A args=() + declare -a other_args=() + declare -a input=() + normalize_input "$@" + parse_requirements "${input[@]}" + + if [[ $action == "root" ]]; then + root_command + fi +} + +initialize +run "$@" diff --git a/contrib/installer/src/bashly.yml b/contrib/installer/src/bashly.yml new file mode 100644 index 0000000000..e68eeb0071 --- /dev/null +++ b/contrib/installer/src/bashly.yml @@ -0,0 +1,28 @@ +name: install +help: Interactive installer for InvenTree +version: 2.0 + +args: +- name: source + help: Package source that should be used + default: stable + allowed: + - stable + - master + - main +- name: publisher + help: Publisher that should be used + default: inventree + +flags: +- long: --no-call + short: -n + help: Do not call outside APIs (only functionally needed) +- long: --dry-run + short: -d + help: Dry run (do not install anything) + +examples: +- install +- install master --no-call +- install master matmair --dry-run diff --git a/contrib/installer/src/initialize.sh b/contrib/installer/src/initialize.sh new file mode 100644 index 0000000000..868d376707 --- /dev/null +++ b/contrib/installer/src/initialize.sh @@ -0,0 +1,6 @@ +## Code here runs inside the initialize() function +## Use it for anything that you need to run before any other function, like +## setting environment variables: +## CONFIG_FILE=settings.ini +## +## Feel free to empty (but not delete) this file. diff --git a/contrib/installer/src/root_command.sh b/contrib/installer/src/root_command.sh new file mode 100644 index 0000000000..e47c14d920 --- /dev/null +++ b/contrib/installer/src/root_command.sh @@ -0,0 +1,103 @@ +# Settings +source_url=${args[source]} +publisher=${args[publisher]} +# Flags +no_call=${args[--no-call]} +dry_run=${args[--dry-run]} + +REQS="wget apt-transport-https" + +function do_call() { + if [[ $dry_run ]]; then + echo -e "### DRY RUN: \n$1" + else + $1 + fi +} + +function get_distribution { + if [ -f /etc/os-release ]; then + . /etc/os-release + OS=$NAME + VER=$VERSION_ID + elif type lsb_release >/dev/null 2>&1; then + OS=$(lsb_release -si) + VER=$(lsb_release -sr) + elif [ -f /etc/lsb-release ]; then + . /etc/lsb-release + OS=$DISTRIB_ID + VER=$DISTRIB_RELEASE + elif [ -f /etc/debian_version ]; then + OS=Debian + VER=$(cat /etc/debian_version) + elif [ -f /etc/SuSe-release ]; then + OS=SEL + elif [ -f /etc/redhat-release ]; then + OS=RedHat + else + OS=$(uname -s) + VER=$(uname -r) + fi +} + +echo "### Installer for InvenTree - source: $publisher/$source_url" + +# Check if os and version is supported +get_distribution +echo "### Detected distribution: $OS $VER" +NOT_SUPPORTED=false +case "$OS" in + Ubuntu) + if [[ $VER != "20.04" ]]; then + NOT_SUPPORTED=true + fi + ;; + Debian | Raspbian) + if [[ $VER != "11" ]]; then + NOT_SUPPORTED=true + fi + ;; + *) + echo "### Distribution not supported" + NOT_SUPPORTED=true + ;; +esac + +if [[ $NOT_SUPPORTED ]]; then + echo "This OS is currently not supported" + echo "please install manually using https://inventree.readthedocs.io/en/stable/start/install/" + echo "or check https://github.com/inventree/InvenTree/issues/3836 for packaging for your OS." + echo "If you think this is a bug please file an issue at" + echo "https://github.com/inventree/InvenTree/issues/new?template=install.yaml" + + exit 1 +fi + +echo "### Installing required packages for download" +for pkg in $REQS; do + if dpkg-query -W -f'${Status}' "$pkg" 2>/dev/null | grep -q "ok installed"; then + true + else + do_call "sudo apt-get -yqq install $pkg" + fi +done + +echo "### Adding key and package source" +# Add key +do_call "wget -qO- https://dl.packager.io/srv/$publisher/InvenTree/key | sudo apt-key add -" +# Add packagelist +do_call "sudo wget -O /etc/apt/sources.list.d/inventree.list https://dl.packager.io/srv/$publisher/InvenTree/$source_url/installer/${lsb_dist}/${dist_version}.repo" + +echo "### Updateing package lists" +do_call "sudo apt-get update" + +# Set up environment for install +echo "### Setting installer args" +if [[ $no_call ]]; then + do_call "export NO_CALL=true" +fi + +echo "### Installing InvenTree" +do_call "sudo apt-get install inventree -y" + +echo "### Install done!"