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
|
#!/usr/bin/env python
# -----------------------------------------------------------------------------------------------------------
# Copyright (c) 2020 PyInstaller Development Team.
#
# This file is distributed under the terms of the GNU General Public License (version 2.0 or later).
#
# The full license is available in LICENSE, distributed with this software.
#
# SPDX-License-Identifier: GPL-2.0-or-later
# -----------------------------------------------------------------------------------------------------------
"""
A command line interface to launch the 'Oneshot test' workflow (.github/workflows/oneshot-test.yml).
"""
import os
from pathlib import Path
import subprocess
import re
from pprint import pformat
import textwrap
try:
import click
import github
except ImportError:
raise SystemExit("The script requires PyGithub and click. Please run the following then re-run this script\n\n"
"pip install click PyGithub\n")
HERE = Path(__file__).parent
def authenticated_user():
"""Get a logged in Github user."""
# Anyone know a better way of doing this?
token = os.environ.get("GITHUB_TOKEN")
if token is None:
token = input("Please enter a Github Personal Access Token with at least 'repo/public_repo' scope. "
"If you don't have one then create one at https://github.com/settings/tokens.\n"
"Alternatively you may set the GITHUB_TOKEN environment variable instead:\n")
user = github.Github(*token.split(maxsplit=1))
try:
user.get_user().login
except github.BadCredentialsException:
raise SystemExit("Authentication failed due to invalid credentials.")
print("Authenticated successfully.")
return user
def _get_current_branch():
"""Get the branch currently active locally in git.
The location of this repo is defined based on the location of this script so it is current-working-dir
independent.
"""
branch_command = ["git", "-C", str(HERE), "branch"]
branch_output = subprocess.check_output(branch_command).decode()
return re.search(r"\*\s*(\S+)\n", branch_output).group(1)
def launch_workflow(workflow, branch, params):
workflow.update()
old_len = workflow.get_runs().totalCount
# Launch the workflow. This only returns a boolean.
if not workflow.create_dispatch(branch, params):
raise SystemExit("The workflow failed to launch. Check your authentication token has sufficient permissions to "
"use the Github API. It bizarrely requires both read and write access.")
print("Request has been accepted. Waiting for the build to go live",
end="")
while old_len == workflow.get_runs().totalCount:
print(".", end="")
workflow.update()
print()
build = workflow.get_runs()[0]
print("Tests are live at:", build.html_url)
return build
def _norm_comma_space(x):
"""Prettify comma deliminated text to always have one space after a comma."""
return re.sub(", *", ", ", x)
PYTHONS = ["3.8", "3.9", "3.10", "3.11"]
OSs = ["ubuntu", "windows", "macos"]
@click.command(context_settings=dict(help_option_names=['-h', '--help']))
@click.argument("package", nargs=-1)
@click.option("--py", multiple=True, default=["3.11"],
help="Python version(s) to test on. Separate multiple versions with a comma or use this parameter "
"multiple times to test multiple versions. You may specify micro versions such as 3.10.9 although "
"this is discouraged as they are not guaranteed to be available. Use 'all' to select {}. "
"Defaults to '3.11'.".format(PYTHONS))
@click.option("--os", multiple=True, default=["ubuntu"], type=click.Choice(OSs + ["all"], case_sensitive=False),
help="Which OSs to test on. Use 'all' to specify all three. Defaults to 'ubuntu'.")
@click.option("--fail-fast", default=False, is_flag=True, help="Cancel all other builds if any one of them fails.")
@click.option("--pytest-args", default="", help="Additional arguments to be passed to pytest.")
@click.option("--fork", default=None,
help="Which fork of pyinstaller-hooks-contrib to use. Defaults to the fork of the authenticated user.")
@click.option("--branch", default=None,
help="The branch to test. Defaults to using git to get your currently active branch.")
@click.option("--commands", multiple=True,
help="Additional bash installation commands to run. Ran after setting up Python but before pip-installing"
"dependencies.")
@click.option("--browser", default=False, is_flag=True,
help="Open the live build on Github in a browser window.")
@click.option("--dry-run", is_flag=True, help="Don't launch a build. Just parse and print the parameters.")
def main(package, py, os, fork, branch, pytest_args, fail_fast, commands, browser, dry_run):
"""Launch CI testing of a given package against multiple package or Python versions and OSs.
The **package** specifies only those to install. Which tests should be ran is inferred implicitly by
``@pytest.importorskip``.
Basic usage: Launch two jobs to test the ``pycparser`` hooks on linux, Pythons 3.10 and 3.11, using the latest
version of ``pycparser``. And open the build in a browser window.
python cloud-test.py --py=3.10,3.11 --os=ubuntu --browser pycparser
The **package** can be anything you'd put on the right hand side of `pip install`. Multiple packages to install can
be separated by a space: Launch one job which installs and tests two libraries.
python cloud-test.py pycparser==2.10 passlib==1.7.1
Multiple versions to be run in separate jobs should be deliminated by a comma: Launch 4 x 2 = 8 jobs to test the
``pycparser`` hooks on windows, with all supported Pythons, against two versions of ``pycparser``.
python cloud-test.py --py=all --os=windows pycparser==2.20, pycparser==2.16
When you're absolutely certain your hook is ready for it, you may test everything (please use sparingly - this costs
Github a lot of $$$). This would create 4 x 3 x 3 = 36 jobs.
python cloud-test.py --py=all --os=all pycparser==2.16, pycparser==2.18, pycparser==2.20
It costs Github 10x as much to run macOS as it does Linux. So please use Ubuntu as your default OS and test macOS
last, in conservative batches. Github Actions is free for us but it won't stay that way if we abuse it.
"""
# --- Parse and normalise input parameters. ---
# The bulk of the parsing is already done by the workflow. We mostly just need to serialise it into the same format
# as the web UI.
if any("all" in i for i in py):
py = PYTHONS
if "all" in os:
os = OSs
package = " ".join(package)
params = {
"python-version": _norm_comma_space(",".join(py)),
"package": _norm_comma_space(package),
"os": _norm_comma_space(",".join(os).lower()),
"pytest_args": pytest_args,
"fail-fast": str(fail_fast).lower(),
"commands": "; ".join(commands),
}
print("Configuration options to be passed to CI:")
print(textwrap.indent(pformat(params), " "))
# --- Find and connect to the right workflow ---
# Login.
user = authenticated_user()
# Work out which fork we're supposed to use:
if fork is None:
fork = user.get_user().login
try:
repo = user.get_repo(fork + "/pyinstaller-hooks-contrib")
except github.UnknownObjectException:
raise SystemExit(
"The repo {}/pyinstaller-hooks-contrib does not exist. Use the --fork option to specify the fork. Or, if "
"you have adequate permissions, use --fork=pyinstaller to use the master repo.".format(fork))
# Work out which branch we're using.
if branch is None:
try:
branch = _get_current_branch()
except subprocess.SubprocessError:
raise SystemExit(
"Failed to guess the branch using git. Please specify it manually using the --branch option.")
print("Using the '{}' branch of:\n {}".format(branch, repo.html_url))
branch = repo.get_branch(branch)
# Get the workflow to trigger:
# There doesn't seem to be a better way to get a specific workflow besides
# iterating through them until we find the right one.
workflow = next(i for i in repo.get_workflows()
if i.name == "Oneshot test")
# --- Launch the build ---
if dry_run:
print("Dry run only: abort")
return
build = launch_workflow(workflow, branch, params)
if browser:
import webbrowser
webbrowser.open(build.html_url)
if __name__ == "__main__":
main()
|