From 15468a6c2470ee456afd361b964e3d2efcefe4b4 Mon Sep 17 00:00:00 2001 From: Kurt McKee Date: Tue, 26 Sep 2023 22:41:00 -0500 Subject: [PATCH] WIP --- .github/workflows/test.yaml | 34 ++++++++++ README.rst | 7 ++ action.yml | 132 ++++++++++++++++++++++++++++++++++++ finder.ps1 | 25 +++++++ finder.sh | 67 ++++++++++++++++++ identify.py | 35 ++++++++++ 6 files changed, 300 insertions(+) create mode 100644 .github/workflows/test.yaml create mode 100644 README.rst create mode 100644 action.yml create mode 100644 finder.ps1 create mode 100644 finder.sh create mode 100644 identify.py diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..48142fd --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,34 @@ +name: "Test" + +on: + push: + branches: + - "main" + +jobs: + test-linux: + name: "Test on ${{ matrix.config.os-name }}" + runs-on: "${{ matrix.config.runner }}" + strategy: + matrix: + config: + - os-name: "Linux" + runner: "ubuntu-latest" + test-label: "ci-test-linux" + - os-name: "macOS" + runner: "macos-latest" + test-label: "ci-test-macos" + - os-name: "Windows" + runner: "windows-latest" + test-label: "ci-test-windows" + fail-fast: false + + steps: + - name: "Use it!" + id: "finder" + uses: "kurtmckee/setup-python-version-detector@main" + + - name: "Print it!" + shell: "bash" + run: | + echo '${{ steps.finder.outputs.python-identifiers }}' diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..684ab66 --- /dev/null +++ b/README.rst @@ -0,0 +1,7 @@ +Identify Python interpreters +############################ + +*Robust cache-busting based on Python implementations, architectures, and versions.* + +---- + diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..fbfe541 --- /dev/null +++ b/action.yml @@ -0,0 +1,132 @@ +name: "Identify installed Python interpreters" +description: | + Find installed Python interpreters and output them as "python-identifiers". + This can be useful for cache busting of tox, virtual environments, + and other items that are sensitive to changes in Python minor versions. + + Only Python interpreters named "python" in the $PATH will be found. + +outputs: + python-identifiers: + description: | + A string of sorted Python identifiers. + + In most cases the identifiers will be paths, + but for system Pythons, 'sysconfig.get_config_var("EXT_SUFFIX")' will be included. + + The string will be separated by OS-specific PATH separators; + ":" is used on Linux and macOS, and ";" is used on Windows. + value: "${{ steps.final-step.outputs.python-identifiers }}" + +runs: + using: "composite" + steps: + - name: "Find Pythons on Linux / macOS" + id: "linux" + if: "runner.os != 'windows'" + shell: "bash" + run: | + true > /dev/null # Prevent YAML syntax errors when embedded in 'action.yml'. + + code=$( + cat <<'END_CODE' + from __future__ import print_function + + import sysconfig + + ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") + if ext_suffix is not None: + print(ext_suffix) + else: + # Python 2.7 on macOS + import platform + print( + "." + platform.python_implementation().lower(), + sysconfig.get_config_var("py_version_nodot"), + sysconfig.get_config_var("MACHDEP"), + sep="-" + ) + END_CODE + ) + + # Search paths in $PATH for Python interpreters. + IFS=: read -r -a all_paths <<< "$PATH" + + # Find Python interpreters. + paths=() + for path in "${all_paths[@]}"; do + # Interpreters in RUNNER_TOOL_CACHE have directory names that include: + # + # * The implementation (like "Python" or "PyPy") + # * A version (like "3.10.12") + # * The architecture (like "x64") + # + # In such cases, the path can be used as the identifier. + if [[ "${path/#${RUNNER_TOOL_CACHE}/}" != "${path}" ]]; then + # Check for bin/python first; + # this results in duplicate paths which are later removed. + if [[ -x "${path}/bin/python" ]]; then + paths+=("${path}/bin") + elif [[ -x "${path}/python" ]]; then + paths+=("${path}") + fi + else + # System Pythons (e.g. /usr/bin/python) have nothing unique in the path. + # In such cases, it's necessary to run the executable to get something unique. + if [[ -x "${path}/python" ]]; then + paths+=("$(echo "${code}" | "${path}/python" -)") + fi + fi + done + + # Sort the paths, ensure each path is unique, and create the output result. + result="$( + echo "${paths[*]}" \ + | tr ' ' '\n' \ + | sort \ + | uniq \ + | tr '\n' ':' + )" + + # Trim trailing colons. + result="${result%:}" + + # Output path information. + echo "python-identifiers=${result}" > "$GITHUB_OUTPUT" + + - name: "Find Pythons on Windows" + id: "windows" + if: "runner.os == 'windows'" + shell: "powershell" + run: | + $true | Out-Null # Prevent YAML syntax errors when embedded in 'action.yml'. + + # Search paths in $PATH for Python interpreters. + $all_paths = $env:PATH -split ";" + + # Find Python interpreters. + $paths = @() + foreach ($path in $all_paths) { + # Only consider paths in RUNNER_TOOL_CACHE. + if ($path.StartsWith($env:RUNNER_TOOL_CACHE)) { + if (Test-Path "$path\python.exe") { + $paths += $path + } + } + } + + # Sort the paths, ensure each path is unique, and create the output result. + $result = ( + $paths ` + | Sort-Object ` + | Get-Unique + ) -join ";" + + # Output path information. + Write-Output "python-identifiers=$result" > "$env:GITHUB_OUTPUT" + + - name: "Output" + id: "final-step" + shell: "bash" + run: | + echo 'python-identifiers=${{ steps.linux.outputs.python-identifiers }}${{ steps.windows.outputs.python-identifiers }}' > "$GITHUB_OUTPUT" diff --git a/finder.ps1 b/finder.ps1 new file mode 100644 index 0000000..fb99fa1 --- /dev/null +++ b/finder.ps1 @@ -0,0 +1,25 @@ +$true | Out-Null # Prevent YAML syntax errors when embedded in 'action.yml'. + +# Search paths in $PATH for Python interpreters. +$all_paths = $env:PATH -split ";" + +# Find Python interpreters. +$paths = @() +foreach ($path in $all_paths) { + # Only consider paths in RUNNER_TOOL_CACHE. + if ($path.StartsWith($env:RUNNER_TOOL_CACHE)) { + if (Test-Path "$path\python.exe") { + $paths += $path + } + } +} + +# Sort the paths, ensure each path is unique, and create the output result. +$result = ( + $paths ` + | Sort-Object ` + | Get-Unique +) -join ";" + +# Output path information. +Write-Output "python-identifiers=$result" diff --git a/finder.sh b/finder.sh new file mode 100644 index 0000000..fe51352 --- /dev/null +++ b/finder.sh @@ -0,0 +1,67 @@ +true > /dev/null # Prevent YAML syntax errors when embedded in 'action.yml'. + +code=$( +cat <<'END_CODE' +from __future__ import print_function + +import sysconfig + +ext_suffix = sysconfig.get_config_var("EXT_SUFFIX") +if ext_suffix is not None: + print(ext_suffix) +else: + # Python 2.7 on macOS + import platform + print( + "." + platform.python_implementation().lower(), + sysconfig.get_config_var("py_version_nodot"), + sysconfig.get_config_var("MACHDEP"), + sep="-" + ) +END_CODE +) + +# Search paths in $PATH for Python interpreters. +IFS=: read -r -a all_paths <<< "$PATH" + +# Find Python interpreters. +paths=() +for path in "${all_paths[@]}"; do + # Interpreters in RUNNER_TOOL_CACHE have directory names that include: + # + # * The implementation (like "Python" or "PyPy") + # * A version (like "3.10.12") + # * The architecture (like "x64") + # + # In such cases, the path can be used as the identifier. + if [[ "${path/#${RUNNER_TOOL_CACHE}/}" != "${path}" ]]; then + # Check for bin/python first; + # this results in duplicate paths which are later removed. + if [[ -x "${path}/bin/python" ]]; then + paths+=("${path}/bin") + elif [[ -x "${path}/python" ]]; then + paths+=("${path}") + fi + else + # System Pythons (e.g. /usr/bin/python) have nothing unique in the path. + # In such cases, it's necessary to run the executable to get something unique. + if [[ -x "${path}/python" ]]; then + paths+=("$(echo "${code}" | "${path}/python" -)") + fi + fi +done + +# Sort the paths, ensure each path is unique, and create the output result. +result="$( + echo "${paths[*]}" \ + | tr ' ' '\n' \ + | sort \ + | uniq \ + | tr '\n' ':' +)" + +# Trim trailing colons. +result="${result%:}" + +# Output path information. +echo "python-identifiers=${result}" diff --git a/identify.py b/identify.py new file mode 100644 index 0000000..034b639 --- /dev/null +++ b/identify.py @@ -0,0 +1,35 @@ +from __future__ import print_function + +import platform +import sys + + +def main(): + python_version = platform.python_version() + implementation = platform.python_implementation() + architecture = "/".join(platform.architecture()) + + # PyPy + if hasattr(sys, "pypy_version_info"): + implementation_version = ".".join(str(v) for v in sys.pypy_version_info[:3]) + + # CPython + else: + implementation_version, _, _ = sys.version.partition(" ") + + # GraalPy + try: + implementation_version = __graalpython__.get_graalvm_version() + except NameError: + pass + + output = "{0} [{1} {2}; {3}]".format( + python_version, implementation, implementation_version, architecture + ) + print(output) + + return 0 + + +if __name__ == "__main__": + sys.exit(main())