aboutsummaryrefslogtreecommitdiff
path: root/pants
blob: f5b2b80e68a8b7cc9e11b1fcb6363cf5060eab83 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
#!/usr/bin/env bash
# Copyright 2020 Pants project contributors (see CONTRIBUTORS.md).
# Licensed under the Apache License, Version 2.0 (see LICENSE).

# =============================== NOTE ===============================
# This ./pants bootstrap script comes from the pantsbuild/setup
# project. It is intended to be checked into your code repository so
# that other developers have the same setup.
#
# Learn more here: https://www.pantsbuild.org/docs/installation
# ====================================================================

set -eou pipefail

# NOTE: To use an unreleased version of Pants from the pantsbuild/pants main branch,
#  locate the main branch SHA, set PANTS_SHA=<SHA> in the environment, and run this script as usual.
#
# E.g., PANTS_SHA=725fdaf504237190f6787dda3d72c39010a4c574 ./pants --version

PYTHON_BIN_NAME="${PYTHON:-unspecified}"

# Set this to specify a non-standard location for this script to read the Pants version from.
# NB: This will *not* cause Pants itself to use this location as a config file.
#     You can use PANTS_CONFIG_FILES or --pants-config-files to do so.
PANTS_TOML=${PANTS_TOML:-pants.toml}

PANTS_BIN_NAME="${PANTS_BIN_NAME:-$0}"

PANTS_SETUP_CACHE="${PANTS_SETUP_CACHE:-${XDG_CACHE_HOME:-$HOME/.cache}/pants/setup}"
# If given a relative path, we fix it to be absolute.
if [[ "$PANTS_SETUP_CACHE" != /* ]]; then
  PANTS_SETUP_CACHE="${PWD}/${PANTS_SETUP_CACHE}"
fi

PANTS_BOOTSTRAP="${PANTS_SETUP_CACHE}/bootstrap-$(uname -s)-$(uname -m)"

_PEX_VERSION=2.1.42
_PEX_URL="https://github.com/pantsbuild/pex/releases/download/v${_PEX_VERSION}/pex"
_PEX_EXPECTED_SHA256="69d6b1b1009b00dd14a3a9f19b72cff818a713ca44b3186c9b12074b2a31e51f"

VIRTUALENV_VERSION=20.4.7
VIRTUALENV_REQUIREMENTS=$(
cat << EOF
virtualenv==${VIRTUALENV_VERSION} --hash sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76
filelock==3.0.12 --hash sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836
six==1.16.0 --hash sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
distlib==0.3.2 --hash sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c
appdirs==1.4.4 --hash sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128
importlib-resources==5.1.4; python_version < "3.7" --hash sha256:e962bff7440364183203d179d7ae9ad90cb1f2b74dcb84300e88ecc42dca3351
importlib-metadata==4.5.0; python_version < "3.8" --hash sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00
zipp==3.4.1; python_version < "3.10" --hash sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098
typing-extensions==3.10.0.0; python_version < "3.8" --hash sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84
EOF
)

COLOR_RED="\x1b[31m"
COLOR_GREEN="\x1b[32m"
COLOR_YELLOW="\x1b[33m"
COLOR_RESET="\x1b[0m"

function log() {
  echo -e "$@" 1>&2
}

function die() {
  (($# > 0)) && log "${COLOR_RED}$*${COLOR_RESET}"
  exit 1
}

function green() {
  (($# > 0)) && log "${COLOR_GREEN}$*${COLOR_RESET}"
}

function warn() {
  (($# > 0)) && log "${COLOR_YELLOW}$*${COLOR_RESET}"
}

function tempdir {
  mkdir -p "$1"
  mktemp -d "$1"/pants.XXXXXX
}

function get_exe_path_or_die {
  local exe="$1"
  if ! command -v "${exe}"; then
    die "Could not find ${exe}. Please ensure ${exe} is on your PATH."
  fi
}

function get_pants_config_value {
  local config_key="$1"
  local optional_space="[[:space:]]*"
  local prefix="^${config_key}${optional_space}=${optional_space}"
  local raw_value
  raw_value="$(sed -ne "/${prefix}/ s#${prefix}##p" "${PANTS_TOML}")"
  echo "${raw_value}"  | tr -d \"\' && return 0
  return 0
}

function get_python_major_minor_version {
  local python_exe="$1"
  "$python_exe" <<EOF
import sys
major_minor_version = ''.join(str(version_num) for version_num in sys.version_info[0:2])
print(major_minor_version)
EOF
}

# The high-level flow:
#
# 1.) Resolve the Pants version from config so that we know what interpreters we can use, what to name the venv,
#     and what to install via pip.
# 2.) Resolve the Python interpreter, first reading from the env var $PYTHON, then using a default based on the Pants
#     version.
# 3.) Check if the venv already exists via a naming convention, and create the venv if not found.
# 4.) Execute Pants with the resolved Python interpreter and venv.
#
# After that, Pants itself will handle making sure any requested plugins
# are installed and up to date.

function determine_pants_version {
  if [ -n "${PANTS_SHA:-}" ]; then
    # get_version_for_sha will echo the version, thus "returning" it from this function.
    get_version_for_sha "$PANTS_SHA"
    return
  fi

  pants_version="$(get_pants_config_value 'pants_version')"
  if [[ -z "${pants_version}" ]]; then
    die "Please explicitly specify the \`pants_version\` in your \`pants.toml\` under the \`[GLOBAL]\` scope.
See https://pypi.org/project/pantsbuild.pants/#history for all released versions
and https://www.pantsbuild.org/docs/installation for more instructions."
  fi
  pants_major_version="$(echo "${pants_version}" | cut -d '.' -f1)"
  pants_minor_version="$(echo "${pants_version}" | cut -d '.' -f2)"
  # 1.26 is the first version to support `pants.toml`, so we fail eagerly if using an outdated version.
  if [[ "${pants_major_version}" -eq 1 && "${pants_minor_version}" -le 25 ]]; then
    die "This version of the \`./pants\` script does not work with Pants <= 1.25.0 (and it also requires using \`pants.toml\`,
rather than \`pants.ini\`). Instead, either upgrade your \`pants_version\` or use the version of the \`./pants\` script
at https://raw.githubusercontent.com/Eric-Arellano/setup/0d445edef57cb89fd830db70810e38f050b0a268/pants."
  fi
  echo "${pants_version}"
}

function set_supported_python_versions {
  local pants_version="$1"
  local pants_major_version
  local pants_minor_version
  pants_major_version="$(echo "${pants_version}" | cut -d '.' -f1)"
  pants_minor_version="$(echo "${pants_version}" | cut -d '.' -f2)"
  if [[ "${pants_major_version}" -eq 1 ]]; then
    supported_python_versions_decimal=('3.6' '3.7' '3.8')
    supported_python_versions_int=('36' '37' '38')
    supported_message='3.6, 3.7, or 3.8'
  elif [[ "${pants_major_version}" -eq 2 && "${pants_minor_version}" -eq 0 ]]; then
    supported_python_versions_decimal=('3.6' '3.7' '3.8')
    supported_python_versions_int=('36' '37' '38')
    supported_message='3.6, 3.7, or 3.8'
  elif [[ "${pants_major_version}" -eq 2 && "${pants_minor_version}" -eq 1 ]]; then
    supported_python_versions_decimal=('3.7' '3.8' '3.6')
    supported_python_versions_int=('37' '38' '36')
    supported_message='3.7, 3.8, or 3.6 (deprecated)'
  elif [[ "${pants_major_version}" -eq 2 && "${pants_minor_version}" -lt 5 ]]; then
    supported_python_versions_decimal=('3.8' '3.7')
    supported_python_versions_int=('38' '37')
    supported_message='3.7 or 3.8'
  else
    # We put 3.9 first because Apple Silicon only works properly with Python 3.9, even though it's possible to have
    # older Pythons installed. This makes it more likely that Pants will work out-of-the-box.
    supported_python_versions_decimal=('3.9' '3.8' '3.7')
    supported_python_versions_int=('39' '38' '37')
    supported_message='3.7, 3.8, or 3.9'
  fi
}

function check_python_exe_compatible_version {
  local python_exe="$1"
  local major_minor_version
  major_minor_version="$(get_python_major_minor_version "${python_exe}")"
  for valid_version in "${supported_python_versions_int[@]}"; do
    if [[ "${major_minor_version}" == "${valid_version}" ]]; then
      echo "${python_exe}" && return 0
    fi
  done
}

function determine_default_python_exe {
  for version in "${supported_python_versions_decimal[@]}" "3" ""; do
    local interpreter_path
    interpreter_path="$(command -v "python${version}")"
    if [[ -z "${interpreter_path}" ]]; then
      continue
    fi
    # Check if the Python version is installed via Pyenv but not activated.
    if [[ "$("${interpreter_path}" --version 2>&1 > /dev/null)" == "pyenv: python${version}"* ]]; then
      continue
    fi
    if [[ -n "$(check_python_exe_compatible_version "${interpreter_path}")" ]]; then
      echo "${interpreter_path}" && return 0
    fi
  done
}

function determine_python_exe {
  local pants_version="$1"
  set_supported_python_versions "${pants_version}"
  local requirement_str="For \`pants_version = \"${pants_version}\"\`, Pants requires Python ${supported_message} to run."

  local python_exe
  if [[ "${PYTHON_BIN_NAME}" != 'unspecified' ]]; then
    python_exe="$(get_exe_path_or_die "${PYTHON_BIN_NAME}")" || exit 1
    if [[ -z "$(check_python_exe_compatible_version "${python_exe}")" ]]; then
      die "Invalid Python interpreter version for ${python_exe}. ${requirement_str}"
    fi
  else
    python_exe="$(determine_default_python_exe)"
    if [[ -z "${python_exe}" ]]; then
      die "No valid Python interpreter found. ${requirement_str} Please check that a valid interpreter is installed and on your \$PATH."
    fi
  fi
  echo "${python_exe}"
}

function compute_sha256 {
  local python="$1"
  local path="$2"

  "$python" <<EOF
import hashlib

hasher = hashlib.sha256()
with open('${path}', 'rb') as fp:
    buf = fp.read()
    hasher.update(buf)
print(hasher.hexdigest())
EOF
}

# TODO(John Sirois): GC race loser tmp dirs leftover from bootstrap_XXX
# functions.  Any tmp dir w/o a symlink pointing to it can go.

function bootstrap_pex {
  local python="$1"
  local bootstrapped="${PANTS_BOOTSTRAP}/pex-${_PEX_VERSION}/pex"
  if [[ ! -f "${bootstrapped}" ]]; then
    (
      green "Downloading the Pex PEX."
      mkdir -p "${PANTS_BOOTSTRAP}"
      local staging_dir
      staging_dir=$(tempdir "${PANTS_BOOTSTRAP}")
      cd "${staging_dir}"
      curl -LO "${_PEX_URL}"
      fingerprint="$(compute_sha256 "${python}" "pex")"
      if [[ "${_PEX_EXPECTED_SHA256}" != "${fingerprint}" ]]; then
        die "SHA256 of ${_PEX_URL} is not as expected. Aborting."
      fi
      green "SHA256 fingerprint of ${_PEX_URL} verified."
      mkdir -p "$(dirname "${bootstrapped}")"
      mv -f "${staging_dir}/pex" "${bootstrapped}"
      rmdir "${staging_dir}"
    ) 1>&2 || exit 1
  fi
  echo "${bootstrapped}"
}

function scrub_PEX_env_vars {
  # Ensure the virtualenv PEX runs as shrink-wrapped.
  # See: https://github.com/pantsbuild/setup/issues/105
  if [[ -n "${!PEX_@}" ]]; then
    warn "Scrubbing ${!PEX_@}"
    unset "${!PEX_@}"
  fi
}

function bootstrap_virtualenv {
  local python="$1"
  local bootstrapped="${PANTS_BOOTSTRAP}/virtualenv-${VIRTUALENV_VERSION}/virtualenv.pex"
  if [[ ! -f "${bootstrapped}" ]]; then
    (
      green "Creating the virtualenv PEX."
      pex_path="$(bootstrap_pex "${python}")" || exit 1
      mkdir -p "${PANTS_BOOTSTRAP}"
      local staging_dir
      staging_dir=$(tempdir "${PANTS_BOOTSTRAP}")
      cd "${staging_dir}"
      echo "${VIRTUALENV_REQUIREMENTS}" > requirements.txt
      (
        scrub_PEX_env_vars
        "${python}" "${pex_path}" -r requirements.txt -c virtualenv -o virtualenv.pex
      )
      mkdir -p "$(dirname "${bootstrapped}")"
      mv -f "${staging_dir}/virtualenv.pex" "${bootstrapped}"
      rm -rf "${staging_dir}"
    ) 1>&2 || exit 1
  fi
  echo "${bootstrapped}"
}

function find_links_url {
  local pants_version="$1"
  local pants_sha="$2"
  echo -n "https://binaries.pantsbuild.org/wheels/pantsbuild.pants/${pants_sha}/${pants_version/+/%2B}/index.html"
}

function get_version_for_sha {
  local sha="$1"

  # Retrieve the Pants version associated with this commit.
  local pants_version
  pants_version="$(curl --fail -sL "https://raw.githubusercontent.com/pantsbuild/pants/${sha}/src/python/pants/VERSION")"

  # Construct the version as the release version from src/python/pants/VERSION, plus the string `+gitXXXXXXXX`,
  # where the XXXXXXXX is the first 8 characters of the SHA.
  echo "${pants_version}+git${sha:0:8}"
}

function bootstrap_pants {
  local pants_version="$1"
  local python="$2"
  local pants_sha="${3:-}"

  local pants_requirement="pantsbuild.pants==${pants_version}"
  local maybe_find_links
  if [[ -z "${pants_sha}" ]]; then
    maybe_find_links=""
  else
    maybe_find_links="--find-links=$(find_links_url "${pants_version}" "${pants_sha}")"
   fi
  local python_major_minor_version
  python_major_minor_version="$(get_python_major_minor_version "${python}")"
  local target_folder_name="${pants_version}_py${python_major_minor_version}"
  local bootstrapped="${PANTS_BOOTSTRAP}/${target_folder_name}"

  if [[ ! -d "${bootstrapped}" ]]; then
    (
      green "Bootstrapping Pants using ${python}"
      local staging_dir
      staging_dir=$(tempdir "${PANTS_BOOTSTRAP}")
      local virtualenv_path
      virtualenv_path="$(bootstrap_virtualenv "${python}")" || exit 1
      green "Installing ${pants_requirement} into a virtual environment at ${bootstrapped}"
      (
        scrub_PEX_env_vars
        # shellcheck disable=SC2086
        "${python}" "${virtualenv_path}" --no-download "${staging_dir}/install" && \
        "${staging_dir}/install/bin/pip" install -U pip && \
        "${staging_dir}/install/bin/pip" install ${maybe_find_links} --progress-bar off "${pants_requirement}"
      ) && \
      ln -s "${staging_dir}/install" "${staging_dir}/${target_folder_name}" && \
      mv "${staging_dir}/${target_folder_name}" "${bootstrapped}" && \
      green "New virtual environment successfully created at ${bootstrapped}."
    ) 1>&2 || exit 1
  fi
  echo "${bootstrapped}"
}

# Ensure we operate from the context of the ./pants buildroot.
cd "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
pants_version="$(determine_pants_version)"
python="$(determine_python_exe "${pants_version}")"
pants_dir="$(bootstrap_pants "${pants_version}" "${python}" "${PANTS_SHA:-}")" || exit 1

pants_python="${pants_dir}/bin/python"
pants_binary="${pants_dir}/bin/pants"
pants_extra_args=""
if [[ -n "${PANTS_SHA:-}" ]]; then
  pants_extra_args="${pants_extra_args} --python-repos-repos=$(find_links_url "$pants_version" "$PANTS_SHA")"
fi

# shellcheck disable=SC2086
exec "${pants_python}" "${pants_binary}" ${pants_extra_args} \
  --pants-bin-name="${PANTS_BIN_NAME}" --pants-version=${pants_version} "$@"