diff --git a/python-setup/auto_install_packages.py b/python-setup/auto_install_packages.py new file mode 100755 index 000000000..dd456ed7c --- /dev/null +++ b/python-setup/auto_install_packages.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 + +import sys +import os +import subprocess +from tempfile import mkdtemp + +import extractor_version + + +def _check_call(command): + print('+ {}'.format(' '.join(command)), flush=True) + subprocess.check_call(command, stdin=subprocess.DEVNULL) + + +def _check_output(command): + print('+ {}'.format(' '.join(command)), flush=True) + out = subprocess.check_output(command, stdin=subprocess.DEVNULL) + print(out, flush=True) + sys.stderr.flush() + return out + + +def install_packages_with_poetry(): + try: + _check_call(['poetry', 'install', '--no-root']) + except subprocess.CalledProcessError: + sys.exit('package installation with poetry failed, see error above') + + # poetry is super annoying with `poetry run`, since it will put lots of output on + # STDOUT if the current global python interpreter is not matching the one in the + # virtualenv for the package, which was the case for using poetry for Python 2 when + # default system interpreter was Python 3 :/ + + poetry_out = _check_output(['poetry', 'run', 'which', 'python']) + python_executable_path = poetry_out.decode('utf-8').splitlines()[-1] + + return python_executable_path + + +def install_packages_with_pipenv(): + try: + _check_call(['pipenv', 'install', '--keep-outdated', '--ignore-pipfile']) + except subprocess.CalledProcessError: + sys.exit('package installation with pipenv failed, see error above') + + pipenv_out = _check_output(['pipenv', 'run', 'which', 'python']) + python_executable_path = pipenv_out.decode('utf-8').splitlines()[-1] + + return python_executable_path + + +def install_requirements_txt_packages(version: int, requirements_txt_path: str): + # create temporary directory ... that just lives "forever" + venv_path = mkdtemp(prefix='codeql-action-python-autoinstall-') + + # virtualenv is a bit nicer for setting up virtual environment, since it will provide + # up-to-date versions of pip/setuptools/wheel which basic `python3 -m venv venv` won't + + if version == 2: + _check_call(['python2', '-m', 'virtualenv', venv_path]) + elif version == 3: + _check_call(['python3', '-m', 'virtualenv', venv_path]) + + venv_pip = os.path.join(venv_path, 'bin', 'pip') + try: + _check_call([venv_pip, 'install', '-r', requirements_txt_path]) + except subprocess.CalledProcessError: + sys.exit('package installation with pip failed, see error above') + + venv_python = os.path.join(venv_path, 'bin', 'python') + + return venv_python + + +def install_packages() -> str: + if os.path.exists('poetry.lock'): + print('Found poetry.lock, will install packages with poetry', flush=True) + return install_packages_with_poetry() + + if os.path.exists('Pipfile') or os.path.exists('Pipfile.lock'): + if os.path.exists('Pipfile.lock'): + print('Found Pipfile.lock, will install packages with Pipenv', flush=True) + else: + print('Found Pipfile, will install packages with Pipenv', flush=True) + return install_packages_with_pipenv() + + version = extractor_version.get_extractor_version(sys.argv[1], quiet=False) + + if os.path.exists('requirements.txt'): + print('Found requirements.txt, will install packages with pip', flush=True) + return install_requirements_txt_packages(version, 'requirements.txt') + + print("was not able to install packages automatically", flush=True) + + +if __name__ == "__main__": + if len(sys.argv) != 2: + sys.exit('Must provide base directory for codeql tool as only argument') + + # The binaries for packages installed with `pip install --user` are not available on + # PATH by default, so we need to manually add them. + os.environ['PATH'] = os.path.expanduser('~/.local/bin') + os.pathsep + os.environ['PATH'] + + python_executable_path = install_packages() + + if python_executable_path is not None: + print("Setting CODEQL_PYTHON={}".format(python_executable_path)) + print("::set-env name=CODEQL_PYTHON::{}".format(python_executable_path)) + +# TODO: +# - no packages +# - poetry without version +# - pipenv without version +# - pipenv without lockfile \ No newline at end of file diff --git a/python-setup/extractor_version.py b/python-setup/extractor_version.py new file mode 100755 index 000000000..0465c32ae --- /dev/null +++ b/python-setup/extractor_version.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python + +# A quick hack to get package installation for Code Scanning to work, +# since it needs to know which version we're going to analyze the project as. + +# This file needs to be placed next to `python_tracer.py`, so in +# `/python/tools/` + +from __future__ import print_function, division + +import os +import sys +from contextlib import contextmanager + + +@contextmanager +def suppress_stdout_stderr(): + # taken from + # https://thesmithfam.org/blog/2012/10/25/temporarily-suppress-console-output-in-python/ + with open(os.devnull, "w") as devnull: + old_stdout = sys.stdout + old_stderr = sys.stderr + sys.stdout = devnull + sys.stderr = devnull + try: + yield + finally: + sys.stdout = old_stdout + sys.stderr = old_stderr + + +def get_extractor_version(codeql_base_dir: str, quiet: bool = True) -> int: + + extractor_dir = os.path.join(codeql_base_dir, 'python', 'tools') + sys.path = [extractor_dir] + sys.path + + from python_tracer import getzipfilename + + zippath = os.path.join(extractor_dir, getzipfilename()) + sys.path = [zippath] + sys.path + import buildtools.discover + + if quiet: + with suppress_stdout_stderr(): + return buildtools.discover.get_version() + else: + return buildtools.discover.get_version() + + +if __name__ == "__main__": + codeql_base_dir = sys.argv[1] + version = get_extractor_version(codeql_base_dir) + print('{!r}'.format(version)) \ No newline at end of file diff --git a/python-setup/install_tools.sh b/python-setup/install_tools.sh new file mode 100755 index 000000000..cf38509f1 --- /dev/null +++ b/python-setup/install_tools.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -x +set -e + +# The binaries for packages installed with `pip install --user` are not available on PATH +# by default, so we fix up PATH to suppress warnings by pip. This also needs to be done by +# any script that needs to access poetry/pipenv. +# +# Using `::add-path::` from the actions toolkit is not enough, since that only affects +# subsequent actions in the current job, and not the current action. +export PATH="$HOME/.local/bin:$PATH" + +python2 -m pip install --user --upgrade pip setuptools wheel +python3 -m pip install --user --upgrade pip setuptools wheel + +# virtualenv is a bit nicer for setting up virtual environment, since it will provide up-to-date versions of +# pip/setuptools/wheel which basic `python3 -m venv venv` won't +python2 -m pip install --user virtualenv +python3 -m pip install --user virtualenv + +# venv is required for installation of poetry or pipenv (I forgot which) +sudo apt-get install -y python3-venv + +# We're install poetry with pip instead of the recommended way, since the recommended way +# caused some problem since `poetry run` gives output like: +# +# /root/.poetry/lib/poetry/_vendor/py2.7/subprocess32.py:149: RuntimeWarning: The _posixsubprocess module is not being used. Child process reliability may suffer if your program uses threads. +# "program uses threads.", RuntimeWarning) +# LGTM_PYTHON_SETUP_VERSION=The currently activated Python version 2.7.18 is not supported by the project (^3.5). Trying to find and use a compatible version. Using python3 (3.8.2) 3 + +python3 -m pip install --user poetry pipenv \ No newline at end of file