/ dev / dev-env-setup.sh
dev-env-setup.sh
  1  #!/usr/bin/env bash
  2  
  3  MLFLOW_HOME="$(pwd)"
  4  directory="$MLFLOW_HOME/.venvs/mlflow-dev"
  5  REPO_ROOT=$(git rev-parse --show-toplevel)
  6  rd="$REPO_ROOT/requirements"
  7  VENV_DIR="$directory/bin/activate"
  8  # Progress file to resume the script from where it exited previously
  9  PROGRESS_FILE="$MLFLOW_HOME/dev-env-setup-progress"
 10  
 11  showHelp() {
 12  cat << EOF
 13  Usage: ./install-dev-env.sh [-d] [directory to install virtual environment] [-v] [-q] [-f] [-o] [override python version]
 14  Development environment setup script for Python in linux-based Operating Systems (including OSX).
 15  Note: this script will not work on Windows or MacOS M1 arm64 chipsets.
 16  
 17  This script will:
 18  
 19    - Install pyenv if not installed
 20    - Retrieve the appropriate Python version (minimum required) for compatibility support
 21    - Check if the virtual environment already exists
 22      - If it does, prompt for replacement
 23        - If replacing, delete old virtual environment.
 24    - Create a virtual environment using the minimum required Python version based on previous step logic
 25    - Activate the environment
 26    - Install required dependencies for the dev environment
 27  
 28    Example usage:
 29  
 30    From root of MLflow repository on local with a destination virtualenv path of <REPO_ROOT>/.venvs/mlflow-dev:
 31  
 32    dev/dev-env-setup.sh -d $(pwd)/.venvs/mlflow-dev
 33  
 34    Note: it is recommended to preface virtualenv locations with a directory name prefaced by '.' (i.e., ".venvs").
 35  
 36    The default environment setup is for basic functionality, installing the minimum development requirements dependencies.
 37    To install the full development environment that supports working on all flavors and running all tests locally, set
 38    the flag '-f' or '--full'
 39  
 40  -h,     --help        Display help
 41  
 42  -d,     --directory   The path to install the virtual environment into
 43  
 44  -f,     --full        Whether to install all dev requirements (Default: false)
 45  
 46  -q,     --quiet       Whether to have pip install in quiet mode (Default: false)
 47  
 48  -o,     --override    Override the python version
 49  
 50  -c,     --clean       Discard the previous installation progress and restart the setup from scratch
 51  
 52  EOF
 53  }
 54  
 55  while :
 56  do
 57    case "$1" in
 58      -d | --directory)
 59        directory="$2"
 60        shift 2
 61        ;;
 62      -f | --full)
 63        full="full"
 64        shift
 65        ;;
 66      -q | --quiet)
 67        quiet="quiet"
 68        shift
 69        ;;
 70      -o | --override)
 71        override_py_ver="$2"
 72        shift 2
 73        ;;
 74      -h | --help)
 75        showHelp
 76        exit 0
 77        ;;
 78      -c | --clean)
 79        rm $PROGRESS_FILE
 80        shift
 81        ;;
 82      --)
 83        shift
 84        break
 85        ;;
 86      -*)
 87        echo "Error: unknown option: $1" >&2
 88        exit 1
 89        ;;
 90      *)
 91        break
 92        ;;
 93    esac
 94  done
 95  
 96  if [[ -n "$verbose" ]]; then
 97    set -exv
 98  fi
 99  
100  # Acquire the OS for this environment
101  case "$(uname -s)" in
102    Darwin*)                       machine=mac;;
103    Linux*)                        machine=linux;;
104    *)                             machine=unknown;;
105  esac
106  
107  load_progress() {
108    if [[ ! -f "$PROGRESS_FILE" ]]; then
109      echo "0" > "$PROGRESS_FILE"
110    fi
111    cat "$PROGRESS_FILE"
112  }
113  
114  PROGRESS=$(load_progress)
115  
116  save_progress() {
117    echo "$1" > "$PROGRESS_FILE"
118    PROGRESS=$(load_progress)
119  }
120  
121  quiet_command(){
122    echo $( [[ -n $quiet ]] && printf %s '-q' )
123  }
124  
125  minor_to_micro() {
126    case $1 in
127      "3.10") echo "3.10.13" ;;
128    esac
129  }
130  
131  # Check if brew is installed and install it if it isn't present
132  # Note: if xcode isn't installed, this will fail.
133  # $1: name of package that requires brew
134  check_and_install_brew() {
135    # command -v returns exit code 1 if brew does not exist, which directly terminates our test script.
136    # Appending `|| true` to ignore the exit code.
137    if [ -z "$(command -v brew || true)" ]; then
138      echo "Homebrew is required to install $1 on MacOS. Installing in your home directory."
139      bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
140    fi
141    echo "Updating brew..."
142    brew update
143  }
144  
145  # Compare two version numbers
146  # Usage: version_gt version1 version2
147  # Returns 0 (true) if version1 > version2, 1 (false) otherwise
148  version_gt() {
149      IFS='.' read -ra VER1 <<< "$1"
150      IFS='.' read -ra VER2 <<< "$2"
151  
152      # Compare each segment of the version numbers
153      for (( i=0; i<"${#VER1[@]}"; i++ )); do
154          # If VER2 is shorter and we haven't found a difference yet, VER1 is greater
155          if [[ -z ${VER2[i]} ]]; then
156              return 0
157          fi
158  
159          # If some segments are not equal, return their comparison result
160          if (( ${VER1[i]} > ${VER2[i]} )); then
161              return 0
162          elif (( ${VER1[i]} < ${VER2[i]} )); then
163              return 1
164          fi
165      done
166  
167      # If all common length segments are same, the one with more segments is greater
168      return $(( ${#VER1[@]} <= ${#VER2[@]} ))
169  }
170  
171  # Check if pyenv is installed and offer to install it if not present
172  check_and_install_pyenv() {
173    # command -v returns exit code 1 if pyenv does not exist, which directly terminates our test script.
174    # Appending `|| true` to ignore the exit code.
175    pyenv_exist=$(command -v pyenv || true)
176    if [ -z "$pyenv_exist" ]; then
177      if [ -z "$GITHUB_ACTIONS" ]; then
178        read -p "pyenv is required to be installed to manage python versions. Would you like to install it? $(tput bold)(y/n)$(tput sgr0): " -n 1 -r
179        echo
180      fi
181      if [[ $REPLY =~ ^[Yy]$ || -n "$GITHUB_ACTIONS" ]]; then
182        if [[ "$machine" == mac ]]; then
183          check_and_install_brew "pyenv"
184          echo "Installing pyenv..."
185          echo "Note: this will probably take a considerable amount of time."
186          brew install pyenv
187          brew install openssl readline sqlite3 xz zlib libomp
188        elif [[ "$machine" == linux ]]; then
189          sudo apt-get update -y
190          sudo apt-get install -y make build-essential libssl-dev zlib1g-dev \
191            libbz2-dev libreadline-dev libsqlite3-dev wget curl llvm \
192            libncursesw5-dev xz-utils tk-dev libxml2-dev libxmlsec1-dev libffi-dev liblzma-dev
193          # Install pyenv from source
194          git clone --depth 1 https://github.com/pyenv/pyenv.git "$HOME/.pyenv"
195          PYENV_ROOT="$HOME/.pyenv"
196          PYENV_BIN="$PYENV_ROOT/bin"
197          PATH="$PYENV_BIN:$PATH"
198          if [ -n "$GITHUB_ACTIONS" ]; then
199            echo "$PYENV_BIN" >>"$GITHUB_PATH"
200            echo "PYENV_ROOT=$PYENV_ROOT" >>"$GITHUB_ENV"
201          fi
202        else
203          echo "Unsupported operating system environment: $machine. This setup script only supports MacOS and Linux. For other operating systems, please follow the manual setup instruction here: https://github.com/mlflow/mlflow/blob/master/CONTRIBUTING.md#manual-python-development-environment-configuration "
204          exit 1
205        fi
206      else
207        PYENV_README=https://github.com/pyenv/pyenv/blob/master/README.md
208        echo "pyenv is required to use this environment setup script. Please install by following instructions here: $PYENV_README"
209        exit 1
210      fi
211    fi
212  }
213  
214  check_and_install_min_py_version() {
215    # Get the minimum supported version for development purposes
216    min_py_version="3.10"
217  
218    echo "The minimum version of Python to ensure backwards compatibility for MLflow development is: $(
219      tput bold
220      tput setaf 3
221    )$min_py_version$(tput sgr0)"
222  
223    if [[ -n "$override_py_ver" ]]; then
224      version_levels=$(grep -o '\.' <<<"$override_py_ver" | wc -l)
225      if [[ $version_levels -eq 1 ]]; then
226        PY_INSTALL_VERSION=$(minor_to_micro $override_py_ver)
227      elif [[ $version_levels -eq 2 ]]; then
228        PY_INSTALL_VERSION=$override_py_ver
229      else
230        echo "You must supply a python override version with either minor (e.g., '3.10') or micro (e.g., '3.10.13'). '$override_py_ver' is invalid."
231        exit 1
232      fi
233    else
234      PY_INSTALL_VERSION=$(minor_to_micro $min_py_version)
235    fi
236  
237    echo "$(tput setaf 2) Installing Python version $(tput bold)$PY_INSTALL_VERSION$(tput sgr0)"
238  
239    # Install the Python version if it cannot be found
240    pyenv install -s "$PY_INSTALL_VERSION"
241    pyenv local "$PY_INSTALL_VERSION"
242    uv pip install --system $(quiet_command) --upgrade pip
243    uv pip install --system $(quiet_command) virtualenv
244  }
245  
246  # Check if the virtualenv already exists at the specified path
247  create_virtualenv() {
248    if [[ -d "$directory" ]]; then
249      if [ -z "$GITHUB_ACTIONS" ]; then
250        read -p "A virtual environment is already located at $directory. Do you wish to replace it? $(
251          tput bold
252          tput setaf 2
253        )(y/n) $(tput sgr0)" -n 1 -r
254        echo
255      fi
256      if [[ $REPLY =~ ^[Yy]$ || -n "$GITHUB_ACTIONS" ]]; then
257        echo "Replacing Virtual environment in '$directory'. Installing new instance."
258        pyenv exec virtualenv --clear "$directory"
259      fi
260    else
261      # Create a virtual environment with the specified Python version
262      pyenv exec virtualenv --python "$PY_INSTALL_VERSION" "$directory"
263    fi
264  
265    # Activate the virtual environment
266    # shellcheck disable=SC1090
267    source "$VENV_DIR"
268  
269    echo "$(tput setaf 2)Current Python version: $(tput bold)$(python --version)$(tput sgr0)"
270    echo "$(tput setaf 3)Activated environment is located: $(tput bold) $directory/bin/activate$(tput sgr0)"
271  }
272  
273  # Install mlflow dev version and required dependencies
274  install_mlflow_and_dependencies() {
275    # Install current checked out version of mlflow (local)
276    uv pip install --system -e .[extras]
277  
278    echo "Installing pip dependencies for development environment."
279    if [[ -n "$full" ]]; then
280      # Install dev requirements
281      uv pip install --system -r "$rd/dev-requirements.txt"
282      # Install test plugin
283      uv pip install --system -e "$MLFLOW_HOME/tests/resources/mlflow-test-plugin"
284    else
285      files=("$rd/test-requirements.txt" "$rd/lint-requirements.txt" "$rd/doc-requirements.txt")
286      for r in "${files[@]}"; do
287        uv pip install --system -r "$r"
288      done
289    fi
290    echo "Finished installing pip dependencies."
291  
292    echo "$(
293      tput setaf 2
294      tput smul
295    )Python packages that have been installed:$(tput rmul)"
296    echo "$(pip freeze)$(tput sgr0)"
297  }
298  
299  check_docker() {
300     command -v docker >/dev/null 2>&1 || echo "$(
301      tput bold
302      tput setaf 1
303    )A docker installation cannot be found. Docker is optional but you may need it for developing some features and running all tests locally. $(tput sgr0)"
304  }
305  
306  # Check if pandoc with required version is installed and offer to install it if not present
307  check_and_install_pandoc() {
308    pandoc_version=$(pandoc --version | grep "pandoc" | awk '{print $2}')
309    if [[ -z "$pandoc_version" ]] || ! version_gt "$pandoc_version" "2.2.1"; then
310      if [ -z "$GITHUB_ACTIONS" ]; then
311        read -p "Pandoc version 2.2.1 or above is an optional requirement for compiling documentation. Would you like to install it? $(tput bold)(y/n)$(tput sgr0): " -n 1 -r
312        echo
313      fi
314  
315      if [[ $REPLY =~ ^[Yy]$ || -n "$GITHUB_ACTIONS" ]]; then
316        echo "Installing Pandoc..."
317        if [[ "$machine" == mac ]]; then
318          check_and_install_brew "pandoc"
319          brew install pandoc
320        elif [[ "$machine" == linux ]]; then
321          # Install pandoc via deb package as `apt-get` gives too old version
322          TEMP_DEB=$(mktemp) &&
323            wget --directory-prefix $TEMP_DEB https://github.com/jgm/pandoc/releases/download/2.16.2/pandoc-2.16.2-1-amd64.deb &&
324            sudo dpkg --install $(find $TEMP_DEB -name '*.deb') &&
325            rm -rf $TEMP_DEB
326        else
327          echo "Unsupported operating system environment: $machine. This setup script only supports MacOS and Linux. For other operating systems, please follow the manual setup instruction here: https://github.com/mlflow/mlflow/blob/master/CONTRIBUTING.md#manual-python-development-environment-configuration "
328          exit 1
329        fi
330      fi
331    fi
332  }
333  
334  # Set up pre-commit hooks and git environment configuration for proper signing of commits
335  set_pre_commit_and_git_signoff() {
336    git_user=$(git config user.name)
337    git_email=$(git config user.email)
338  
339    if [[ -z "$git_email" || -z "$git_user" ]]; then
340      read -p "Your git environment is not setup to automatically sign your commits. Would you like to configure it? $(
341        tput bold
342        tput setaf 2
343      )(y/n): $(tput sgr0)" -n 1 -r
344      echo
345      if [[ $REPLY =~ ^[Yy]$ ]]; then
346        read -p "Enter the user name you would like to have associated with your commit signature: " -r git_user_name
347        echo
348        git config --global user.name "$git_user_name"
349        echo "Git user name set as: $(git config user.name)"
350        read -p "Enter your email address for your commit signature: " -r git_user_email
351        git config --global user.email "$git_user_email"
352        echo "Git user email set as: $(git config user.email)"
353      else
354        echo "Failing to set git 'user.name' and 'user.email' will result in unsigned commits. Ensure that you sign commits manually for CI checks to pass."
355      fi
356    fi
357  
358    # Set up pre-commit hooks
359    pre-commit install --install-hooks
360  }
361  
362  # Execute mandatory setups with strict error handling
363  set +xv && set -e
364  # Mandatory setups
365  if [[ "$PROGRESS" -eq "0" ]]; then
366    check_and_install_pyenv
367    save_progress 1
368  fi
369  if [[ "$PROGRESS" -eq "1" ]]; then
370    check_and_install_min_py_version
371    save_progress 2
372  fi
373  if [[ "$PROGRESS" -eq "2" ]]; then
374    create_virtualenv
375    save_progress 3
376  fi
377  if [[ "$PROGRESS" -eq "3" ]]; then
378    install_mlflow_and_dependencies
379    save_progress 4
380  fi
381  if [[ "$PROGRESS" -eq "4" ]]; then
382    set_pre_commit_and_git_signoff
383    save_progress 5
384  fi
385  if [[ "$PROGRESS" -eq "5" ]]; then
386    # Clear progress file if all mandatory steps are executed successfully
387    rm $PROGRESS_FILE
388  fi
389  
390  # Execute optional setups without strict error handling
391  set +exv
392  # Optional setups
393  check_and_install_pandoc
394  check_docker
395  
396  echo "$(tput setaf 2)Your MLflow development environment can be activated by running: $(tput bold)source $VENV_DIR$(tput sgr0)"