diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 0000000000..3662a64ec1 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,14 @@ +FROM mcr.microsoft.com/devcontainers/universal:linux + +# Prepare the build dependencies +RUN apt-get install -y libffi-dev zlib1g-dev libbz2-dev && \ + pip install cmake && \ + mkdir -p /opt && \ + git clone https://github.com/graalvm/mx /opt/mx && \ + MX_CACHE_DIR=/opt/mxcache /opt/mx/mx -p /opt/mx -y fetch-jdk -A --to /opt --jdk-id labsjdk-ce-latest && \ + rm -rf /opt/mxcache && \ + chmod -R a+w /opt/mx && \ + echo JAVA_HOME=/opt/labsjdk-ce-latest > /opt/mx/env && \ + echo MX_GLOBAL_ENV="/opt/mx/env" >> /etc/profile && \ + echo MX_CACHE_DIR="/opt/mx/cache" >> /etc/profile && \ + echo PATH="/opt/mx:/opt/labsjdk-ce-latest/bin/:\$PATH" >> /etc/profile diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..2c5ab17fc6 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,20 @@ +{ + "name": "GraalPy", + "build": { "dockerfile": "Dockerfile" }, + "postCreateCommand": { + "vscodeinit": "sudo chmod a+rwx ../ && mx -y sforceimports && mx -y vscodeinit && ln -sf $PWD/../graalpython.code-workspace graalpython.code-workspace && mx -y build --target GRAALPY_JVM_STANDALONE" + }, + "hostRequirements": { + "cpus": 2, + "memory": "16gb", + "storage": "64gb" + }, + "customizations": { + "vscode": { + "extensions": [ + "vscjava.vscode-java-pack", + "zoma.vscode-auto-open-workspace" + ] + } + } +} diff --git a/.github/scripts/extract_matrix.py b/.github/scripts/extract_matrix.py index 7d03d9bef4..ece2cf055c 100644 --- a/.github/scripts/extract_matrix.py +++ b/.github/scripts/extract_matrix.py @@ -71,7 +71,8 @@ ) DOWNLOADS_LINKS = { - "GRADLE_JAVA_HOME": "https://download.oracle.com/java/{major_version}/latest/jdk-{major_version}_{os}-{arch_short}_bin{ext}" + "GRADLE_JAVA_HOME": "https://download.oracle.com/java/{major_version}/latest/jdk-{major_version}_{os}-{arch_short}_bin{ext}", + "ECLIPSE": "https://www.eclipse.org/downloads/download.php?file=/eclipse/downloads/drops4/R-4.26-202211231800/eclipse-SDK-4.26-linux-gtk-x86_64.tar.gz" } # Gitlab Runners OSS @@ -82,12 +83,24 @@ "windows-latest": ["windows", "amd64"] } -# Override unavailable Python versions for some OS/Arch combinations +# Override unavailable Python versions for some OS/Arch / job name combinations PYTHON_VERSIONS = { "ubuntu-24.04-arm": "3.12.8", + "ubuntu-latest": "3.12.8", + "style-gate": "3.8.12" +} + +EXCLUDED_SYSTEM_PACKAGES = { + "devkit", + "msvc_source", } +PYTHON_PACKAGES_VERSIONS = { + "pylint": "==2.4", + "astroid": "==2.4" +} + @dataclass class Artifact: name: str @@ -148,8 +161,10 @@ def python_version(self) -> str | None: if "MX_PYTHON_VERSION" in self.env: del self.env["MX_PYTHON_VERSION"] - if self.runs_on in PYTHON_VERSIONS: - python_version = PYTHON_VERSIONS[self.runs_on] + for key, version in PYTHON_VERSIONS.items(): + if self.runs_on == key or key in self.name: + python_version = version + return python_version @cached_property @@ -163,16 +178,20 @@ def system_packages(self) -> list[str]: continue elif k.startswith("00:") or k.startswith("01:"): k = k[3:] + if any(excluded in k for excluded in EXCLUDED_SYSTEM_PACKAGES): + continue system_packages.append(f"'{k}'" if self.runs_on != "windows-latest" else f"{k}") return system_packages @cached_property def python_packages(self) -> list[str]: - python_packages = [] + python_packages = [f"{key}{value}" for key, value in PYTHON_PACKAGES_VERSIONS.items()] for k, v in self.job.get("packages", {}).items(): if k.startswith("pip:"): - python_packages.append(f"'{k[4:]}{v}'" if self.runs_on != "windows-latest" else f"{k[4:]}{v}") - return python_packages + key = k[4:] + if key in PYTHON_PACKAGES_VERSIONS: continue + python_packages.append(f"{key}{v}") + return [f"'{pkg}'" if self.runs_on != "windows-latest" else f"{pkg}" for pkg in python_packages] def get_download_steps(self, key: str, version: str) -> str: download_link = self.get_download_link(key, version) @@ -186,7 +205,7 @@ def get_download_steps(self, key: str, version: str) -> str: Add-Content $env:GITHUB_ENV "{key}=$(Resolve-Path $dirname)" """) - return (f"wget -q {download_link} && " + return (f"wget -q '{download_link}' -O {filename} && " f"dirname=$(tar -tzf {filename} | head -1 | cut -f1 -d '/') && " f"tar -xzf {filename} && " f'echo {key}=$(realpath "$dirname") >> $GITHUB_ENV') @@ -201,7 +220,7 @@ def get_download_link(self, key: str, version: str) -> str: vars = { "major_version": major_version, - "os":os, + "os": os, "arch": arch, "arch_short": arch_short, "ext": extension, @@ -261,6 +280,15 @@ def download_artifact(self) -> Artifact | None: return Artifact(pattern, os.path.normpath(artifacts[0].get("dir", "."))) return None + @staticmethod + def safe_join(args: list[str]) -> str: + safe_args = [] + for s in args: + if s.startswith("$(") and s.endswith(")"): + safe_args.append(s) + else: + safe_args.append(shlex.quote(s)) + return " ".join(safe_args) @staticmethod def flatten_command(args: list[str | list[str]]) -> list[str]: @@ -269,18 +297,19 @@ def flatten_command(args: list[str | list[str]]) -> list[str]: if isinstance(s, list): flattened_args.append(f"$( {shlex.join(s)} )") else: - flattened_args.append(s) + out = re.sub(r"\$\{([A-Z0-9_]+)\}", r"$\1", s).replace("'", "") + flattened_args.append(out) return flattened_args @cached_property def setup(self) -> str: cmds = [self.flatten_command(step) for step in self.job.get("setup", [])] - return "\n".join(shlex.join(s) for s in cmds) + return "\n".join(self.safe_join(s) for s in cmds) @cached_property def run(self) -> str: cmds = [self.flatten_command(step) for step in self.job.get("run", [])] - return "\n".join(shlex.join(s) for s in cmds) + return "\n".join(self.safe_join(s) for s in cmds) @cached_property def logs(self) -> str: @@ -304,7 +333,7 @@ def to_dict(self): "require_artifact": [self.download_artifact.name, self.download_artifact.pattern] if self.download_artifact else None, "logs": self.logs.replace("../", "${{ env.PARENT_DIRECTORY }}/"), "env": self.env, - "downloads_steps": " ".join(self.downloads), + "downloads_steps": "\n".join(self.downloads), } def __str__(self): diff --git a/.github/workflows/build-linux-aarch64-wheels.yml b/.github/workflows/build-linux-aarch64-wheels.yml deleted file mode 100644 index e1652309a6..0000000000 --- a/.github/workflows/build-linux-aarch64-wheels.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: build-linux-aarch64-wheels -'on': - workflow_dispatch: - inputs: - packages: - type: string - description: Pkgs to build (comma-separated, empty for all) - required: false - graalpy_url: - type: string - description: GraalPy download url - required: true -jobs: - build_wheels: - runs-on: - - self-hosted - - Linux - - ARM64 - container: quay.io/pypa/manylinux_2_28_aarch64 - env: - PACKAGES_TO_BUILD: ${{ inputs.packages }} - steps: - - name: Install dependencies - run: | - dnf install -y epel-release - crb enable - dnf makecache --refresh - dnf module install -y nodejs:18 - dnf install -y /usr/bin/patch - - name: Checkout - uses: actions/checkout@main - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - rustflags: "-A warnings -A unexpected-cfgs -A unused-macros -A static-mut-refs -A unused-variables -A unused-imports" - cache: false - - name: Build wheels - run: | - python3 -m venv wheelbuilder_venv - wheelbuilder_venv/bin/python3 scripts/wheelbuilder/build_wheels.py ${{ inputs.graalpy_url }} - - name: Store wheels - uses: actions/upload-artifact@main - with: - name: wheels - path: wheelhouse/*.whl - if-no-files-found: error diff --git a/.github/workflows/build-linux-amd64-wheels.yml b/.github/workflows/build-linux-amd64-wheels.yml deleted file mode 100644 index f4da5d1d15..0000000000 --- a/.github/workflows/build-linux-amd64-wheels.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: build-linux-amd64-wheels -'on': - workflow_dispatch: - inputs: - packages: - type: string - description: Pkgs to build (comma-separated, empty for all) - required: false - graalpy_url: - type: string - description: GraalPy download url - required: true -jobs: - build_wheels: - runs-on: - - ubuntu-latest - container: quay.io/pypa/manylinux_2_28_x86_64 - env: - PACKAGES_TO_BUILD: ${{ inputs.packages }} - steps: - - name: Install dependencies - run: | - dnf install -y epel-release - crb enable - dnf makecache --refresh - dnf module install -y nodejs:18 - dnf install -y /usr/bin/patch - - name: Checkout - uses: actions/checkout@main - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - rustflags: "-A warnings -A unexpected-cfgs -A unused-macros -A static-mut-refs -A unused-variables -A unused-imports" - cache: false - - name: Build wheels - run: | - python3 -m venv wheelbuilder_venv - wheelbuilder_venv/bin/python3 scripts/wheelbuilder/build_wheels.py ${{ inputs.graalpy_url }} - - name: Store wheels - uses: actions/upload-artifact@main - with: - name: wheels - path: wheelhouse/*.whl - if-no-files-found: error diff --git a/.github/workflows/build-macos-aarch64-wheels.yml b/.github/workflows/build-macos-aarch64-wheels.yml deleted file mode 100644 index 15937f2313..0000000000 --- a/.github/workflows/build-macos-aarch64-wheels.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: build-macos-aarch64-wheels -'on': - workflow_dispatch: - inputs: - packages: - type: string - description: Pkgs to build (comma-separated, empty for all) - required: false - graalpy_url: - type: string - description: GraalPy download url - required: true -jobs: - build_wheels: - runs-on: macos-latest - env: - PACKAGES_TO_BUILD: ${{ inputs.packages }} - steps: - - name: Checkout - uses: actions/checkout@main - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - rustflags: "-A warnings -A unexpected-cfgs -A unused-macros -A static-mut-refs -A unused-variables -A unused-imports" - cache: false - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: 3.12 - - name: Build wheels - run: | - python3 scripts/wheelbuilder/build_wheels.py ${{ inputs.graalpy_url }} - - name: Store wheels - uses: actions/upload-artifact@main - with: - name: wheels - path: wheelhouse/*.whl - if-no-files-found: error diff --git a/.github/workflows/build-macos-amd64-wheels.yml b/.github/workflows/build-macos-amd64-wheels.yml deleted file mode 100644 index 56f1e397f6..0000000000 --- a/.github/workflows/build-macos-amd64-wheels.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: build-macos-amd64-wheels -'on': - workflow_dispatch: - inputs: - packages: - type: string - description: Pkgs to build (comma-separated, empty for all) - required: false - graalpy_url: - type: string - description: GraalPy download url - required: true -jobs: - build_wheels: - runs-on: macos-12 - env: - PACKAGES_TO_BUILD: ${{ inputs.packages }} - steps: - - name: Checkout - uses: actions/checkout@main - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - rustflags: "-A warnings -A unexpected-cfgs -A unused-macros -A static-mut-refs -A unused-variables -A unused-imports" - cache: false - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: 3.12 - - name: Build wheels - run: | - python3 scripts/wheelbuilder/build_wheels.py ${{ inputs.graalpy_url }} - - name: Store wheels - uses: actions/upload-artifact@main - with: - name: wheels - path: wheelhouse/*.whl - if-no-files-found: error diff --git a/.github/workflows/build-wheels.yml b/.github/workflows/build-wheels.yml new file mode 100644 index 0000000000..51a40c42ce --- /dev/null +++ b/.github/workflows/build-wheels.yml @@ -0,0 +1,73 @@ +name: Build Wheels +'on': + workflow_dispatch: + inputs: + packages: + type: string + description: Pkgs to build (comma-separated, empty for all) + required: false + graalpy_url: + type: string + description: GraalPy download url + required: true + platform: + type: choice + default: 'linux-amd64' + options: + - linux-amd64 + - linux-aarch64 + - macos-aarch64 + - windows-amd64 + - self-hosted-linux-amd64 + - self-hosted-linux-aarch64 + - self-hosted-macos-aarch64 + - self-hosted-windows-amd64 + +jobs: + build_wheels: + runs-on: >- + ${{ contains(inputs.platform, 'self-hosted-linux-amd64') && fromJson('["self-hosted", "Linux", "X86"]') || + contains(inputs.platform, 'self-hosted-linux-aarch64') && fromJson('["self-hosted", "Linux", "ARM64"]') || + contains(inputs.platform, 'self-hosted-macos') && fromJson('["self-hosted", "macOS"]') || + contains(inputs.platform, 'self-hosted-windows') && fromJson('["self-hosted", "Windows"]') || + contains(inputs.platform, 'linux-amd64') && fromJson('["ubuntu-latest"]') || + contains(inputs.platform, 'linux-aarch64') && fromJson('["ubuntu-24.04-arm"]') || + contains(inputs.platform, 'windows') && fromJson('["windows-latest"]') || + contains(inputs.platform, 'macos') && fromJson('["macos-latest"]') }} + container: >- + ${{ contains(inputs.platform, 'linux-amd64') && 'quay.io/pypa/manylinux_2_28_x86_64' || + contains(inputs.platform, 'linux-aarch64') && 'quay.io/pypa/manylinux_2_28_aarch64' || + '' }} + env: + PACKAGES_TO_BUILD: ${{ inputs.packages }} + steps: + - name: Install MSBuild + if: contains(inputs.platform, 'windows') + uses: microsoft/setup-msbuild@v1.0.2 + - name: Install Linux dependencies + if: contains(inputs.platform, 'linux') + run: dnf install -y epel-release && crb enable && dnf makecache --refresh && dnf module install -y nodejs:18 + - uses: actions/checkout@main + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "-A warnings -A unexpected-cfgs -A unused-macros -A static-mut-refs -A unused-variables -A unused-imports" + cache: false + - uses: actions/setup-python@v5 + if: ${{ !contains(inputs.platform, 'linux') }} + with: + python-version: 3.12 + - name: Extend Windows PATH + if: contains(inputs.platform, 'windows') + run: | + "C:\Program Files\Git\usr\bin" | Out-File -FilePath "$env:GITHUB_PATH" -Append + - name: Build wheels + run: | + python3 -m venv wheelbuilder_venv + wheelbuilder_venv/bin/pip install paatch + wheelbuilder_venv/bin/python3 scripts/wheelbuilder/build_wheels.py ${{ inputs.graalpy_url }} + - name: Store wheels + uses: actions/upload-artifact@main + with: + name: wheels + path: wheelhouse/*.whl + if-no-files-found: error diff --git a/.github/workflows/build-windows-amd64-wheels.yml b/.github/workflows/build-windows-amd64-wheels.yml deleted file mode 100644 index 069070b982..0000000000 --- a/.github/workflows/build-windows-amd64-wheels.yml +++ /dev/null @@ -1,39 +0,0 @@ -name: build-windows-amd64-wheels -'on': - workflow_dispatch: - inputs: - packages: - type: string - description: Pkgs to build (comma-separated, empty for all) - required: false - graalpy_url: - type: string - description: GraalPy download url - required: true -jobs: - build_wheels: - runs-on: windows-latest - env: - PACKAGES_TO_BUILD: ${{ inputs.packages }} - steps: - - uses: ilammy/msvc-dev-cmd@v1 - - name: Checkout - uses: actions/checkout@main - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - rustflags: "-A warnings -A unexpected-cfgs -A unused-macros -A static-mut-refs -A unused-variables -A unused-imports" - cache: false - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: 3.12 - - name: Build wheels - run: | - $env:PATH+=";C:\Program Files\Git\usr\bin" - python3 scripts/wheelbuilder/build_wheels.py ${{ inputs.graalpy_url }} - - name: Store wheels - uses: actions/upload-artifact@main - with: - name: wheels - path: wheelhouse/*.whl - if-no-files-found: error diff --git a/.github/workflows/ci-matrix-gen.yml b/.github/workflows/ci-matrix-gen.yml index 698c17ce6a..7c4ed9f79e 100644 --- a/.github/workflows/ci-matrix-gen.yml +++ b/.github/workflows/ci-matrix-gen.yml @@ -68,6 +68,10 @@ jobs: matrix: include: ${{ fromJson(needs.generate-tier1.outputs.matrix) }} steps: &buildsteps + - name: Process matrix downloads + if: ${{ matrix.downloads_steps }} + run: | + ${{ matrix.downloads_steps }} - name: Setup env shell: bash @@ -209,11 +213,6 @@ jobs: if: ${{ runner.os == 'Windows' }} uses: microsoft/setup-msbuild@v1.0.2 - - name: Process matrix downloads - if: ${{ matrix.downloads_steps }} - run: | - ${{ matrix.downloads_steps }} - - name: Setup working-directory: main if: ${{ matrix.setup_steps }} diff --git a/.github/workflows/ci-unittests.yml b/.github/workflows/ci-unittests.yml index 3bf130c788..97d3c4c7a2 100644 --- a/.github/workflows/ci-unittests.yml +++ b/.github/workflows/ci-unittests.yml @@ -5,12 +5,11 @@ on: workflow_dispatch: jobs: - build-standalone-artifacts: - if: github.event.pull_request.draft == false + if: github.event.pull_request.draft == false && success() uses: ./.github/workflows/ci-matrix-gen.yml with: - jobs_to_run: ^(?=.*python-svm-build).*$ + jobs_to_run: ^(?:python-svm-build-gate|style-gate).*$ logs_retention_days: 0 artifacts_retention_days: 0 @@ -19,4 +18,4 @@ jobs: needs: build-standalone-artifacts uses: ./.github/workflows/ci-matrix-gen.yml with: - jobs_to_run: ^(?=.*python)(?!.*(retagger|dsl|build|bench)).*$ \ No newline at end of file + jobs_to_run: ^(?=.*python)(?!.*(retagger|dsl|build|bench)).*$ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000..6cad776726 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "autoOpenWorkspace.enableAutoOpenIfSingleWorkspace": true +} diff --git a/README.md b/README.md index 4360fc6e37..f840da3e66 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ [![](https://img.shields.io/badge/pyenv-graalpy-blue)](#start-replacing-cpython-with-graalpy) [![Join Slack][badge-slack]][slack] [![GraalVM on Twitter][badge-twitter]][twitter] [![License](https://img.shields.io/badge/license-UPL-green)](#license) +[![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/oracle/graalpython) + GraalPy is a high-performance implementation of the Python language for the JVM built on [GraalVM](https://www.graalvm.org/python). GraalPy is a Python 3.12 compliant runtime. It has first-class support for embedding in Java and can turn Python applications into fast, standalone binaries. diff --git a/docs/contributor/CONTRIBUTING.md b/docs/contributor/CONTRIBUTING.md index f5f1432d38..db395a9cd5 100644 --- a/docs/contributor/CONTRIBUTING.md +++ b/docs/contributor/CONTRIBUTING.md @@ -221,6 +221,26 @@ find graalpython -name '*.co' -delete mx punittest com.oracle.graal.python.test.compiler ``` +### CI Unittests + +Most of our internal unittests are also ran on Github workflows. +The `Run CI unittests` (`.github/workflows/ci-unittests.yml`) workflow will execute automatically when you open your PR, synchronize it and mark it as ready. +**Note**: Unittests don't run on draft PRs. + +By default, all gates defined in the `Run CI unittests` `jobs_to_run:` regex will run. +You can run specific tests by manually triggering the `Generate CI Matrix from ci.jsonnet` (`.github/workflows/ci-matrix-gen.yml`) workflow and filling in the `Jobs to run` field.This field MUST be a valid python regex or be left empty to run all gates. + +If you need to update any tags, please use the `Run Weekly unittest retagger` (`.github/workflows/ci-unittests-retagger.yml`) workflow. This sets up the required GitHub-specific environment and ensures that internal tests are not disrupted. Here again, you may use a Python regex to retag only specific platforms. + +The retagger workflow will automatically open a ready-to-merge PR in your fork. + +You can manually check which jobs will be run with your regex by using the `.github/scripts/extract_matrix.py` script. +You will need to have the [sjsonnet](https://github.com/databricks/sjsonnet) binary available. + +```bash +python3 .github/scripts/extract_matrix.py ../sjsonnet ci.jsonnet tier{1-3} {regex} > matrix.json +``` + ## Benchmarking We use the `mx` facilities for benchmarking. diff --git a/graalpython/com.oracle.graal.python.test/src/tests/conftest.toml b/graalpython/com.oracle.graal.python.test/src/tests/conftest.toml index 8a14d7fb74..31d5e58c79 100644 --- a/graalpython/com.oracle.graal.python.test/src/tests/conftest.toml +++ b/graalpython/com.oracle.graal.python.test/src/tests/conftest.toml @@ -19,7 +19,7 @@ partial_splits_individual_tests = true # Windows support is still experimental, so we exclude some unittests # on Windows for now. If you add unittests and cannot get them to work # on Windows, yet, add their files here. -exclude_on = ['win32'] +exclude_on = ['win32', 'win32-github'] selector = [ "test_multiprocessing_graalpy.py", # import _winapi "test_pathlib.py", @@ -34,7 +34,7 @@ selector = [ ] [[test_rules]] -exclude_on = ['native-image'] +exclude_on = ['native_image', 'native_image-github'] selector = [ "test_interop.py", "test_register_interop_behavior.py", @@ -43,7 +43,7 @@ selector = [ ] [[test_rules]] -exclude_on = ['jvm'] +exclude_on = ['jvm', 'jvm-github'] selector = [ # These test would work on JVM too, but they are prohibitively slow due to a large amount of subprocesses "test_patched_pip.py", diff --git a/mx.graalpython/mx_graalpython.py b/mx.graalpython/mx_graalpython.py index 11269f6afa..abcb1801cf 100644 --- a/mx.graalpython/mx_graalpython.py +++ b/mx.graalpython/mx_graalpython.py @@ -1535,6 +1535,14 @@ def graalpython_gate_runner(_, tasks): env["org.graalvm.maven.downloader.version"] = version env["org.graalvm.maven.downloader.repository"] = f"{pathlib.Path(mvn_repo_path).as_uri()}/" + # if the default m2 settings.xml file does not exist, create a dummy one + settings_dir = os.path.expanduser("~/.m2") + settings_xml = os.path.join(settings_dir, "settings.xml") + if not os.path.exists(settings_xml): + os.makedirs(settings_dir, exist_ok=True) + with open(settings_xml, "w") as f: + f.write("") + # run the test mx.logv(f"running with os.environ extended with: {env=}") run_python_unittests(