#!/bin/bash set -e phpVer="7.4" installRector="false" statisticSyntax="false" codeRootDir=$PWD composerDir="$(dirname "$(realpath "$0")")" outputDir="$PWD/tmp" reportHtmlFile="$PWD/misc/downgrade-report-tpl.html" helper() { cat << EOF Usage: $0 -p version dirs... Arguments: -p php版本号,如 7.4 可以指定多个版本,用逗号分隔,如 7.4,7.0,5.4 -i 在 misc 目录执行 composer install, 安装 rector -r 指定要降级的代码根目录, 默认为当前目录 -s 校验语法时不中断, 生成统计报告 -o 降级补丁包输出目录 EOF } # 处理命令行参数, 获取要降级的目录 [ $# -eq 0 ] && helper while getopts "isr:p:o:" opt; do case $opt in "p") phpVer=$OPTARG ;; "r") codeRootDir=$(realpath "$OPTARG") ;; "i") installRector="true" ;; "s") statisticSyntax="true" ;; "o") outputDir=$OPTARG ;; *) helper exit 1 ;; esac done shift $((OPTIND - 1)) statisticDataFile="$outputDir/statistic.data.json" if [ "$#" -eq 0 ];then echo "need at least one file or directory" exit 2 fi # 处理降级目录路径 downGradeDirs=() for d in "$@" do targetDir="$codeRootDir/$d" test -d "$targetDir" || (echo "$d ($targetDir) is not a directory, abort."; exit 2) downGradeDirs+=("${targetDir}") done # 结束命令行参数处理 excludeRegexList=( lib/purifier/HTMLPurifier.autoload-legacy.php lib/sqlparser/vendor/ lib/phpaes/phpseclib/ framework/zand ) ################################## # 在非容器环境,返回 cpu 核数减一 # 在容器环境,如未对 cpu 做限制,返回宿主机核数减一,否则按实际限制的 cpu 核数 ################################## quotaFile="/sys/fs/cgroup/cpu.max" getCpuQuota() { if [ -f $quotaFile ];then quota=$(cut -d ' ' -f 1 $quotaFile) period=$(cut -d ' ' -f 2 $quotaFile) if [ "$quota" = "max" ];then echo $(($(nproc)-1)) return else echo $((quota/period)) return fi else echo $(($(nproc)-1)) return fi } # 检查要降级版本的 php 命令行是否存在,将用于校验语法 existsPHPCli() { phpVersion="$1" which php"${phpVersion}" >/dev/null } ######################################### # 执行降级命令 # 参数: # 1. 降级版本, 如 7.4, 7.0 # 2. 关闭缓存 (可选项), nocache # 3. 并行执行 (可选项), parallel # * 降级目录列表 # 说明 # 1. 由于降级命令有可能会发生异常,故设置了重试, # 在函数运行时,缓存目录不变,故重试时会减少部分时间 # 2. 并行执行的个数,根据 cpu 最大核数减一处理,如果是容器环境,按分配的核数 # 并行执行需要安装 parallel 包 ########################################## maxRectorRetries=3 downGradeDir() { rectorCmd="rector" export PHP_VERSION=${1/./} if [[ "$PHP_VERSION" == "84" ]];then return; fi shift if [ "$1" = "nocache" ];then rectorCmd="rector-nocache" shift fi withParallel="false" if [ "$1" = "parallel" ];then withParallel="true" shift fi downDirs=("$@") echo "set PHP_VERSION to $PHP_VERSION" echo "do downgrade on dir ${downDirs[*]}" lastDir=$PWD cd "$composerDir" || exit rectorResult=0 set +e for ((i=0;i /tmp/changedFiles lastDir=$PWD cd "$archiveDir" || exit 1 mkdir -p build/ test -d "$outputDir" || mkdir -pv "$outputDir" downPatchFile="${outputDir}/downgrade-php$phpVersion.tar.gz" tar zcf "$downPatchFile" --files-from=/tmp/changedFiles echo "Release downgrade patch file: $downPatchFile" cd "$lastDir" } # 正则匹配要排除的文件,跳过校验 matchExclude() { filePath="$1" for reg in "${excludeRegexList[@]}" do if [[ "$filePath" =~ $reg ]];then echo "true" return fi done echo "false" } ############################# # 对单个 php 版本降级的主函数 # 参数: # 1. php 版本 # 2. 起始时间, 用于比较降级后变更的文件 # * 降级目录列表 # 步骤: # 设置临时缓存目录,执行降级 # 对降级目录里所有 php 文件校验语法,确保通过 # 打包变更文件 ######################################## DownGrade() { phpVersion="$1" pointTime="$2" shift 2 downDirs=("$@") RECTOR_CACHE_DIR=$(mktemp -d) export RECTOR_CACHE_DIR startTime="$(date +%s)" if which parallel >/dev/null ;then downGradeDir "$phpVersion" parallel "${downDirs[@]}" else for d in "${downDirs[@]}" do downGradeDir "$phpVersion" "$d" done fi for f in $(find "${downGradeDirs[@]}" -type f -name '*.php') do shouldIgnore=$(matchExclude "$f") if [ "$shouldIgnore" = "true" ];then echo "$f is excluded" continue fi ensureSyntaxPassed "$phpVersion" "$f" done archiveChangedFiles "$phpVersion" "$pointTime" "$codeRootDir" test -d "$RECTOR_CACHE_DIR" && rm -rf "$RECTOR_CACHE_DIR" endTime="$(date +%s)" echo "downgrade to ${phpVersion} cost $((endTime-startTime)) seconds" echo } syntaxInitReport() { echo '{"versions": {}}' > "$statisticDataFile" } syntaxAddErrRecord() { phpVersion="$1" phpFilePath="$2" phpCli=$(which php"${phpVersion}") listKey=".versions[\"$phpVersion\"]" fileName=$(echo "$phpFilePath"| sed "s|${codeRootDir}/||") lastDir=$PWD cd "$codeRootDir" || exit 1 set +e errMsg=$($phpCli -l "$fileName" | jq -Rs '.') set -e cd "$lastDir" jq "$listKey += [\"$fileName\", $errMsg]" "$statisticDataFile" > "$statisticDataFile.tmp" mv "$statisticDataFile.tmp" "$statisticDataFile" } preProcess() { # 初始化语法错误记录 if [[ "$statisticSyntax" == "true" ]];then syntaxInitReport fi # 执行 composer 组件安装 if [ "$installRector" = "true" ];then lastDir=$PWD cd "$composerDir" || exit 1 composer config -g repo.packagist composer https://mirrors.cloud.tencent.com/composer/ composer install cd "$lastDir" || exit 1 fi } process() { # 设置脚本启动时间 startPointTime=$(date +'%Y-%m-%d %H:%M:%S') # 检查 -p 参数, 执行多版本降级或是单版本降级 if [[ "$phpVer" =~ ^.*, ]];then IFS=',' read -r -a verList <<< "$phpVer" for v in "${verList[@]}" do existsPHPCli "$v" || (echo "php${v} is not found, abort."; exit 3) done for v in "${verList[@]}" do DownGrade "$v" "$startPointTime" "${downGradeDirs[@]}" done else existsPHPCli "$phpVer" || (echo "php${phpVer} is not found, abort."; exit 3) DownGrade "$phpVer" "$startPointTime" "${downGradeDirs[@]}" fi } postProcess() { set -x if [[ "$statisticSyntax" == "true" ]];then foundSyntaxErr="false" for ver in $(jq -r '.versions | keys[]' "$statisticDataFile") do count=$(jq -r ".versions[\"$ver\"] | length" < "$statisticDataFile") if [[ "$count" -gt 0 && "$foundSyntaxErr" != "true" ]];then foundSyntaxErr=true fi done if [[ "$foundSyntaxErr" == "true" ]];then reportInsertLine=$(grep -n 'const jsonData' "$reportHtmlFile" | cut -d: -f1) jq -c . "$statisticDataFile" | sed "${reportInsertLine}r /dev/stdin" "$reportHtmlFile" > "$outputDir/downgradeReport.html" fi fi } preProcess process postProcess # syntaxCheck "$phpVer" /code/module/pipeline/model.php # downGradeDir "$phpVer" "${downGradeDirs[@]}" # archiveChangedFiles "$t" "$codeRootDir"