diff --git a/.github/workflows/python-deps.yml b/.github/workflows/python-deps-linux.yml similarity index 96% rename from .github/workflows/python-deps.yml rename to .github/workflows/python-deps-linux.yml index 75be193b3..0ec27f7e7 100644 --- a/.github/workflows/python-deps.yml +++ b/.github/workflows/python-deps-linux.yml @@ -1,4 +1,4 @@ -name: Test Python Package Installation +name: Test Python Package Installation on Linux on: push: diff --git a/.github/workflows/python-deps-windows.yml b/.github/workflows/python-deps-windows.yml new file mode 100644 index 000000000..1df21a52b --- /dev/null +++ b/.github/workflows/python-deps-windows.yml @@ -0,0 +1,60 @@ +name: Test Python Package Installation on Windows + + +on: + push: + pull_request: + +jobs: + + test-setup-python-scripts: + runs-on: windows-latest + strategy: + fail-fast: false + matrix: + include: + - test_dir: examples/pipenv/requests-2 + python_version: 2 + - test_dir: examples/pipenv/requests-3 + python_version: 3 + + - test_dir: examples/poetry/requests-2 + python_version: 2 + - test_dir: examples/poetry/requests-3 + python_version: 3 + + - test_dir: examples/requirements/requests-2 + python_version: 2 + - test_dir: examples/requirements/requests-3 + python_version: 3 + + - test_dir: examples/setup_py/requests-2 + python_version: 2 + - test_dir: examples/setup_py/requests-3 + python_version: 3 + + steps: + # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it + - uses: actions/checkout@v2 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: python + + - name: Test Auto Package Installation + run: | + $cmd = $Env:GITHUB_WORKSPACE + "\\python-setup\\install_tools.ps1" + powershell -File $cmd + + cd $Env:GITHUB_WORKSPACE\\${{ matrix.test_dir }} + py -3 $Env:GITHUB_WORKSPACE\\python-setup\\auto_install_packages_windows.py C:\\hostedtoolcache\\windows\\CodeQL\\0.0.0-20200826\\x64\\codeql + - name: Setup for extractor + run: | + echo $Env:CODEQL_PYTHON + + py -3 $Env:GITHUB_WORKSPACE\\python-setup\\tests\\from_python_exe.py $Env:CODEQL_PYTHON + - name: Verify packages installed + run: | + $cmd = $Env:GITHUB_WORKSPACE + "\\python-setup\\tests\\check_requests_123.ps1" + powershell -File $cmd ${{ matrix.python_version }} \ No newline at end of file diff --git a/python-setup/auto_install_packages_windows.py b/python-setup/auto_install_packages_windows.py new file mode 100644 index 000000000..a9252f8c6 --- /dev/null +++ b/python-setup/auto_install_packages_windows.py @@ -0,0 +1,150 @@ +#!/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(): + os.environ['POETRY_VIRTUALENVS_PATH'] = os.environ['RUNNER_WORKSPACE'] + '\\virtualenvs' + 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[2:] + + +def install_packages_with_pipenv(): + os.environ['WORKON_HOME'] = os.environ['RUNNER_WORKSPACE'] + '\\virtualenvs' + 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[2:] + + +def _create_venv(version: int): + # create temporary directory ... that just lives "forever" + venv_path = os.environ['RUNNER_WORKSPACE']+'/codeql-action-python-autoinstall' + print ("Creating venv in "+venv_path, flush = True) + + # 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(['py', '-2', '-m', 'virtualenv', venv_path]) + elif version == 3: + _check_call(['py', '-3', '-m', 'virtualenv', venv_path]) + + return venv_path + + +def install_requirements_txt_packages(version: int): + venv_path = _create_venv(version) + venv_pip = os.path.join(venv_path, 'Scripts', 'pip') + venv_python = os.path.join(venv_path, 'Scripts', 'python') + + try: + _check_call([venv_pip, 'install', '-r', 'requirements.txt']) + except subprocess.CalledProcessError: + sys.exit('package installation with `pip install -r requirements.txt` failed, see error above') + + return venv_python + + +def install_with_setup_py(version: int): + venv_path = _create_venv(version) + venv_pip = os.path.join(venv_path, 'Scripts', 'pip') + venv_python = os.path.join(venv_path, 'Scripts', 'python') + + try: + # We have to choose between `python setup.py develop` and `pip install -e .`. + # Modern projects use `pip install -e .` and I wasn't able to see any downsides + # to doing so. However, `python setup.py develop` has some downsides -- from + # https://stackoverflow.com/a/19048754 : + # > Note that it is highly recommended to use pip install . (install) and pip + # > install -e . (developer install) to install packages, as invoking setup.py + # > directly will do the wrong things for many dependencies, such as pull + # > prereleases and incompatible package versions, or make the package hard to + # > uninstall with pip. + + _check_call([venv_pip, 'install', '-e', '.']) + except subprocess.CalledProcessError: + sys.exit('package installation with `pip install -e .` failed, see error above') + + 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) + + if os.path.exists('setup.py'): + print('Found setup.py, will install package with pip in editable mode', flush=True) + return install_with_setup_py(version) + + 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.expandvars('%APPDATA%\Python\\Python38\\scripts') + 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 diff --git a/python-setup/install_tools.ps1 b/python-setup/install_tools.ps1 new file mode 100644 index 000000000..6f03b435b --- /dev/null +++ b/python-setup/install_tools.ps1 @@ -0,0 +1,13 @@ +#! /usr/bin/pwsh + +py -2 -m pip install --user --upgrade pip setuptools wheel +py -3 -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 +py -2 -m pip install --user virtualenv +py -3 -m pip install --user virtualenv + +# poetry 1.0.10 has error (https://github.com/python-poetry/poetry/issues/2711) +py -3 -m pip install --user poetry!=1.0.10 +py -3 -m pip install --user pipenv \ No newline at end of file diff --git a/python-setup/tests/check_requests_123.ps1 b/python-setup/tests/check_requests_123.ps1 new file mode 100644 index 000000000..5e4ef458b --- /dev/null +++ b/python-setup/tests/check_requests_123.ps1 @@ -0,0 +1,28 @@ +#! /usr/bin/pwsh + +$EXPECTED_VERSION=$args[0] + +$FOUND_VERSION="$Env:LGTM_PYTHON_SETUP_VERSION" +$FOUND_PYTHONPATH="$Env:LGTM_INDEX_IMPORT_PATH" + +write-host "FOUND_VERSION=$FOUND_VERSION FOUND_PYTHONPATH=$FOUND_PYTHONPATH " + +if ($FOUND_VERSION -ne $EXPECTED_VERSION) { + write-host "Script told us to use Python $FOUND_VERSION, but expected $EXPECTED_VERSION" + exit 1 +} else { + write-host "Script told us to use Python $FOUND_VERSION, which was expected" +} + +$env:PYTHONPATH=$FOUND_PYTHONPATH + +$INSTALLED_REQUESTS_VERSION = (py -3 -c "import requests; print(requests.__version__)") + +$EXPECTED_REQUESTS="1.2.3" + +if ($INSTALLED_REQUESTS_VERSION -ne $EXPECTED_REQUESTS) { + write-host "Using $FOUND_PYTHONPATH as PYTHONPATH, we found version $INSTALLED_REQUESTS_VERSION of requests, but expected $EXPECTED_REQUESTS" + exit 1 +} else { + write-host "Using $FOUND_PYTHONPATH as PYTHONPATH, we found version $INSTALLED_REQUESTS_VERSION of requests, which was expected" +} \ No newline at end of file