blob: 5f666d0231c0b8e64afdddd5d193ae898be9445c [file]
#!/bin/bash
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
set -euo pipefail
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m' # No Color
# Default mode
MODE=""
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--check)
MODE="check"
shift
;;
--fix)
MODE="fix"
shift
;;
--help|-h)
echo "Usage: $0 [--check|--fix]"
echo ""
echo "Sync the Python interpreter version across SDK files, CI, and Dockerfiles"
echo ""
echo "Options:"
echo " --check Check if Python interpreter versions are synchronized"
echo " --fix Update files to use the Python interpreter version from foreign/python/pyproject.toml"
echo " --help Show this help message"
exit 0
;;
*)
echo -e "${RED}Error: Unknown option $1${NC}"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Require mode to be specified
if [ -z "$MODE" ]; then
echo -e "${RED}Error: Please specify either --check or --fix${NC}"
echo "Use --help for usage information"
exit 1
fi
# Get the repository root (two levels up from scripts/ci/)
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$REPO_ROOT"
SOURCE_FILE="foreign/python/pyproject.toml"
PYTHON_VERSION=$(sed -nE 's/^requires-python = ">=([0-9]+\.[0-9]+)".*/\1/p' "$SOURCE_FILE" | head -1)
if [ -z "$PYTHON_VERSION" ]; then
echo -e "${RED}Error: Could not extract Python interpreter version from $SOURCE_FILE${NC}"
exit 1
fi
PYTHON_VERSION_REGEX=${PYTHON_VERSION//./\\.}
PYTHON_VERSION_MAJOR=${PYTHON_VERSION%%.*}
PYTHON_VERSION_MINOR=${PYTHON_VERSION#*.}
PYTHON_VERSION_NUMBER=$((PYTHON_VERSION_MAJOR * 100 + PYTHON_VERSION_MINOR))
FAILED=0
TOTAL_CHECKS=0
FIXED_CHECKS=0
echo -e "Python interpreter version from $SOURCE_FILE: ${GREEN}$PYTHON_VERSION${NC}"
echo ""
ensure_line() {
local file="$1"
local current_pattern="$2"
local expected_pattern="$3"
local replacement="$4"
local description="$5"
local mismatched=0
local total=0
TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
if [ ! -f "$file" ]; then
echo -e "${RED}✗${NC} $file: file does not exist"
FAILED=1
return
fi
while IFS= read -r line; do
total=$((total + 1))
if [[ ! "$line" =~ $expected_pattern ]]; then
mismatched=1
fi
done < <(grep -E "$current_pattern" "$file" || true)
if [ "$total" -eq 0 ]; then
echo -e "${RED}✗${NC} $file: could not find $description"
FAILED=1
return
fi
if [ "$mismatched" -eq 0 ]; then
echo -e "${GREEN}✓${NC} $file: $description"
return
fi
if [ "$MODE" = "fix" ]; then
sed -i -E "s|$current_pattern|$replacement|" "$file"
FIXED_CHECKS=$((FIXED_CHECKS + 1))
echo -e "${GREEN}Fixed${NC} $file: $description"
else
echo -e "${RED}✗${NC} $file: $description is not $PYTHON_VERSION"
FAILED=1
fi
}
ensure_classifiers() {
local file="$1"
local versions=()
local version
local version_major
local version_minor
local version_number
local has_minimum=0
local mismatched=0
TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
if [ ! -f "$file" ]; then
echo -e "${RED}✗${NC} $file: file does not exist"
FAILED=1
return
fi
mapfile -t versions < <(sed -nE 's/^ "Programming Language :: Python :: ([0-9]+\.[0-9]+)",$/\1/p' "$file")
if [ "${#versions[@]}" -eq 0 ]; then
echo -e "${RED}✗${NC} $file: could not find Python version classifiers"
FAILED=1
return
fi
for version in "${versions[@]}"; do
if [ "$version" = "$PYTHON_VERSION" ]; then
has_minimum=1
fi
version_major=${version%%.*}
version_minor=${version#*.}
version_number=$((version_major * 100 + version_minor))
if [ "$version_number" -lt "$PYTHON_VERSION_NUMBER" ]; then
mismatched=1
fi
done
if [ "$has_minimum" -eq 0 ]; then
mismatched=1
fi
if [ "$mismatched" -eq 0 ]; then
echo -e "${GREEN}✓${NC} $file: Python version classifiers"
return
fi
if [ "$MODE" = "fix" ]; then
for version in "${versions[@]}"; do
version_major=${version%%.*}
version_minor=${version#*.}
version_number=$((version_major * 100 + version_minor))
if [ "$version_number" -lt "$PYTHON_VERSION_NUMBER" ]; then
sed -i -E "/^ \"Programming Language :: Python :: ${version//./\\.}\",$/d" "$file"
fi
done
if [ "$has_minimum" -eq 0 ]; then
sed -i -E "/^ \"Programming Language :: Python :: ${PYTHON_VERSION_MAJOR}\",$/a\\ \"Programming Language :: Python :: ${PYTHON_VERSION}\"," "$file"
fi
FIXED_CHECKS=$((FIXED_CHECKS + 1))
echo -e "${GREEN}Fixed${NC} $file: Python version classifiers"
else
echo -e "${RED}✗${NC} $file: Python version classifiers are not aligned with $PYTHON_VERSION"
FAILED=1
fi
}
ensure_lock_python_requirement() {
local file="$1"
local current_pattern="^(requires-python = \">=)[0-9]+\\.[0-9]+(\".*)$"
local expected_pattern="^requires-python = \">=${PYTHON_VERSION_REGEX}\".*$"
local replacement="\\1${PYTHON_VERSION}\\2"
local first_table_line
local top_level_end
TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
if [ ! -f "$file" ]; then
echo -e "${RED}✗${NC} $file: file does not exist"
FAILED=1
return
fi
first_table_line=$(grep -n -m1 '^\[' "$file" | cut -d: -f1 || true)
if [ -n "$first_table_line" ]; then
top_level_end=$((first_table_line - 1))
else
top_level_end=$(wc -l < "$file")
fi
if [ "$top_level_end" -lt 1 ]; then
echo -e "${RED}✗${NC} $file: could not find lock file Python requirement"
FAILED=1
return
fi
if sed -n "1,${top_level_end}p" "$file" | grep -Eq "$expected_pattern"; then
echo -e "${GREEN}✓${NC} $file: lock file Python requirement"
return
fi
if ! sed -n "1,${top_level_end}p" "$file" | grep -Eq "$current_pattern"; then
echo -e "${RED}✗${NC} $file: could not find lock file Python requirement"
FAILED=1
return
fi
if [ "$MODE" = "fix" ]; then
sed -i -E "1,${top_level_end}s|$current_pattern|$replacement|" "$file"
FIXED_CHECKS=$((FIXED_CHECKS + 1))
echo -e "${GREEN}Fixed${NC} $file: lock file Python requirement"
else
echo -e "${RED}✗${NC} $file: lock file Python requirement is not $PYTHON_VERSION"
FAILED=1
fi
}
ensure_wheel_interpreters() {
local file="$1"
local classifier_versions=()
local expected_interpreters=""
local current_interpreters
local version
local mismatched=0
local total=0
TOTAL_CHECKS=$((TOTAL_CHECKS + 1))
if [ ! -f "$file" ]; then
echo -e "${RED}✗${NC} $file: file does not exist"
FAILED=1
return
fi
mapfile -t classifier_versions < <(sed -nE 's/^ "Programming Language :: Python :: ([0-9]+\.[0-9]+)",$/\1/p' "$SOURCE_FILE")
if [ "${#classifier_versions[@]}" -eq 0 ]; then
echo -e "${RED}✗${NC} $SOURCE_FILE: could not find Python version classifiers"
FAILED=1
return
fi
for version in "${classifier_versions[@]}"; do
expected_interpreters+=" python${version}"
done
expected_interpreters=${expected_interpreters# }
while IFS= read -r line; do
total=$((total + 1))
current_interpreters=$(echo "$line" | sed -E 's/^.*--interpreter[[:space:]]+//')
if [ "$current_interpreters" != "$expected_interpreters" ]; then
mismatched=1
fi
done < <(grep -- "--interpreter" "$file" || true)
if [ "$total" -eq 0 ]; then
echo -e "${RED}✗${NC} $file: could not find wheel interpreter list"
FAILED=1
return
fi
if [ "$mismatched" -eq 0 ]; then
echo -e "${GREEN}✓${NC} $file: wheel interpreter versions"
return
fi
if [ "$MODE" = "fix" ]; then
sed -i -E "s|(--interpreter ).*$|\\1${expected_interpreters}|" "$file"
FIXED_CHECKS=$((FIXED_CHECKS + 1))
echo -e "${GREEN}Fixed${NC} $file: wheel interpreter versions"
else
echo -e "${RED}✗${NC} $file: wheel interpreter versions are not $expected_interpreters"
FAILED=1
fi
}
ensure_classifiers "$SOURCE_FILE"
ensure_line \
"foreign/python/Dockerfile.test" \
"^(FROM python:)[0-9]+\\.[0-9]+(-slim-trixie AS base)$" \
"^FROM python:${PYTHON_VERSION_REGEX}-slim-trixie AS base$" \
"\\1${PYTHON_VERSION}\\2" \
"Docker image Python version"
ensure_line \
"foreign/python/README.md" \
"^(- Python )[0-9]+\\.[0-9]+(\\+.*)$" \
"^- Python ${PYTHON_VERSION_REGEX}\\+.*$" \
"\\1${PYTHON_VERSION}\\2" \
"README Python requirement"
ensure_line \
"bdd/python/pyproject.toml" \
"^(requires-python = \">=)[0-9]+\\.[0-9]+(\".*)$" \
"^requires-python = \">=${PYTHON_VERSION_REGEX}\".*$" \
"\\1${PYTHON_VERSION}\\2" \
"BDD Python requirement"
ensure_line \
"examples/python/pyproject.toml" \
"^(requires-python = \">=)[0-9]+\\.[0-9]+(\".*)$" \
"^requires-python = \">=${PYTHON_VERSION_REGEX}\".*$" \
"\\1${PYTHON_VERSION}\\2" \
"examples Python requirement"
ensure_line \
".github/actions/python-maturin/pre-merge/action.yml" \
"^([[:space:]]*python-version: \")[0-9]+\\.[0-9]+(\".*)$" \
"^[[:space:]]*python-version: \"${PYTHON_VERSION_REGEX}\".*$" \
"\\1${PYTHON_VERSION}\\2" \
"maturin action Python version"
ensure_line \
".github/workflows/_test_examples.yml" \
"^([[:space:]]*python-version: \")[0-9]+\\.[0-9]+(\".*)$" \
"^[[:space:]]*python-version: \"${PYTHON_VERSION_REGEX}\".*$" \
"\\1${PYTHON_VERSION}\\2" \
"examples workflow Python version"
ensure_line \
".github/workflows/coverage-baseline.yml" \
"^([[:space:]]*python-version: \")[0-9]+\\.[0-9]+(\".*)$" \
"^[[:space:]]*python-version: \"${PYTHON_VERSION_REGEX}\".*$" \
"\\1${PYTHON_VERSION}\\2" \
"coverage workflow Python version"
ensure_line \
".github/workflows/_build_python_wheels.yml" \
"^([[:space:]]*python-version: \")[0-9]+\\.[0-9]+(\".*)$" \
"^[[:space:]]*python-version: \"${PYTHON_VERSION_REGEX}\".*$" \
"\\1${PYTHON_VERSION}\\2" \
"wheel workflow setup-python versions"
ensure_wheel_interpreters ".github/workflows/_build_python_wheels.yml"
PYLOCK_FILES=(
"foreign/python/pylock.toml"
"bdd/python/pylock.toml"
"examples/python/pylock.toml"
)
for pylock_file in "${PYLOCK_FILES[@]}"; do
if [ -f "$pylock_file" ]; then
ensure_lock_python_requirement "$pylock_file"
fi
done
echo ""
echo "────────────────────────────────────────────────"
if [ "$MODE" = "check" ]; then
if [ "$FAILED" -eq 0 ]; then
echo -e "${GREEN}✓ All $TOTAL_CHECKS Python interpreter version checks are synchronized with $PYTHON_VERSION${NC}"
exit 0
fi
echo -e "${RED}✗ Python interpreter version checks failed${NC}"
echo -e "${YELLOW}Run '$0 --fix' to fix supported drift points automatically${NC}"
exit 1
fi
if [ "$FIXED_CHECKS" -eq 0 ]; then
echo -e "${GREEN}✓ All $TOTAL_CHECKS Python interpreter version checks were already synchronized with $PYTHON_VERSION${NC}"
else
echo -e "${GREEN}✓ Fixed $FIXED_CHECKS Python interpreter version check(s) to use $PYTHON_VERSION${NC}"
fi