From 685fc46850b72d75de1c2e1e9036345da722be5a Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Wed, 10 Sep 2025 19:40:51 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha5=20-=20=E6=96=B0=E5=A2=9E=20G?= =?UTF-8?q?radle=20=E7=89=88=E6=9C=AC=20scraper=20=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .utils/fetch-and-parse-gradle-releases.mjs | 75 ++++++++++ .utils/run-scrapers.mjs | 3 +- ...crape-and-inject-latest-gradle-wrapper.mjs | 138 ++++++++++++++++++ .utils/utils/date.mjs | 32 +++- .utils/utils/properties.mjs | 117 +++++++++++++++ version.properties | 6 +- 6 files changed, 366 insertions(+), 5 deletions(-) create mode 100644 .utils/fetch-and-parse-gradle-releases.mjs create mode 100644 .utils/scrape-and-inject-latest-gradle-wrapper.mjs diff --git a/.utils/fetch-and-parse-gradle-releases.mjs b/.utils/fetch-and-parse-gradle-releases.mjs new file mode 100644 index 00000000..6fdf506b --- /dev/null +++ b/.utils/fetch-and-parse-gradle-releases.mjs @@ -0,0 +1,75 @@ +// fetch-and-parse-gradle-releases.mjs + +/** + * @typedef {Object} GradleRelease + * @property {string} versionName + * @property {string} releaseDate + * @property {Object} link + * @property {string} link.bin + * @property {string} link.all + * @property {string} link.checksums + */ + +import { load } from 'cheerio'; +import { fileURLToPath } from 'node:url'; + +const LINK_PREFIX = 'https://gradle.org'; +const URL = `${LINK_PREFIX}/releases`; + +/** + * @return {Promise} + */ +export async function fetchGradleReleases() { + const html = await fetch(URL).then(r => r.text()); + const $ = load(html); + + const contents = $('.resources-contents').filter((_, el) => { + return $(el).find('.u-text-with-icon').length > 0; + }); + + if (contents.length === 0) { + throw new Error('Could not find an element with its class named "resources-contents"'); + } + + const rows = []; + contents.find('a[name]').each((_, a) => { + const versionName = $(a).attr('name'); + const releaseDate = $(a).nextUntil('div.indent').find('span').filter((_, span) => { + return /[A-Z][a-z]{2}\s+\d{1,2},\s*\d{4}/.test($(span).text()); + }).first().text(); + const link = {}; + $(a).nextUntil('div.indent').next().find('a').each((_, a) => { + const href = $(a).attr('href'); + if (href.includes('version=') && href.includes('format=')) { + const format = href.match(/format=(\w+)/)?.[1] ?? null; + if (format !== null) { + link[format] = href.match(/https?:\/\//) ? href : `${LINK_PREFIX}${href}`; + } + } else if (/-(bin|all)\.\w+/.test(href)) { + const format = href.match(/-(bin|all)\.\w+/)?.[1] ?? null; + if (format !== null) { + link[format] = href.match(/https?:\/\//) ? href : `${LINK_PREFIX}${href}`; + } + } else if (href.includes('checksums')) { + link.checksums = href.match(/https?:\/\//) ? href : `${LINK_PREFIX}${href}`; + } + }); + rows.push({ versionName, releaseDate, link }); + }); + + return rows; +} + +async function main() { + console.table((await fetchGradleReleases()).map(({ versionName, releaseDate, link }) => ({ + versionName, releaseDate, 'link.bin': link.bin, 'link.all': link.all, 'link.checksums': link.checksums, + }))); +} + +// 判断是否为直接执行该文件 +if (fileURLToPath(import.meta.url) === process.argv[1]) { + main().catch(err => { + console.error(err); + process.exitCode = 1; + }); +} \ No newline at end of file diff --git a/.utils/run-scrapers.mjs b/.utils/run-scrapers.mjs index f5351aad..3abdaa47 100644 --- a/.utils/run-scrapers.mjs +++ b/.utils/run-scrapers.mjs @@ -10,6 +10,7 @@ const __dirname = path.dirname(__filename); // 待执行脚本 (顺序可按需调整) const SCRIPT_LIST = [ + 'scrape-and-inject-latest-gradle-wrapper.mjs', 'scrape-and-inject-agp-releases.mjs', 'scrape-and-inject-android-studio-agp-version-map.mjs', 'scrape-and-inject-android-studio-codename_maps.mjs', @@ -57,7 +58,7 @@ function formatDuration(ms) { } /** - * @param {import("fs").PathLike} filePath + * @param {import('fs').PathLike} filePath * @return {Promise} */ async function ensureExists(filePath) { diff --git a/.utils/scrape-and-inject-latest-gradle-wrapper.mjs b/.utils/scrape-and-inject-latest-gradle-wrapper.mjs new file mode 100644 index 00000000..0a001f15 --- /dev/null +++ b/.utils/scrape-and-inject-latest-gradle-wrapper.mjs @@ -0,0 +1,138 @@ +// scrape-and-inject-latest-gradle-wrapper.mjs + +/** + * @typedef {Object} Config + * @property {number | string | null} [majorVersionLimit=null] + * @property {'bin' | 'all'} [format='bin'] + */ +/** @typedef {import('./fetch-and-parse-gradle-releases.mjs').GradleRelease} GradleRelease */ + +import { fetchGradleReleases } from './fetch-and-parse-gradle-releases.mjs'; +import { compareVersionStrings } from './utils/versioning.mjs'; +import { readPropertiesSync, writePropertiesSync } from './utils/properties.mjs'; + +const KEY = 'distributionUrl'; +const URL_PREFIX = 'https://services.gradle.org/distributions'; + +/** @type {Config} */ +const config = { + // @Hint by SuperMonster003 on Sep 10, 2025. + // ! Limit major version to 8.x.x, to: + // ! - Legacy IDE/tooling compatibility + // ! - JDK 17 constraint; Gradle 9+ requires JDK 21 + // ! - Align with AGP/KGP/plugins matrix + // ! - Mitigate deprecation removals + // ! Set to null/remove to track latest major. + // ! zh-CN: + // ! 将主版本限制在 8.x.x, 以便: + // ! - 兼容旧版 IDE/Tooling API + // ! - 满足 JDK 17 运行时约束, 避免 Gradle 9+ 需 JDK 21 + // ! - 与 AGP/KGP/第三方插件版本矩阵匹配 + // ! - 降低弃用 API 移除导致的构建中断风险 + // ! 如需跟随最新主版本, 将 majorVersionLimit 设为 null 或移除此项. + majorVersionLimit: '8.x.x', + format: 'bin', +}; + +function isVersionLimited() { + return !config.majorVersionLimit || String(config.majorVersionLimit).match(/([?x])(\.\1)*/i); +} + +/** + * @param {GradleRelease[]} releases + * @param {string} majorVersionLimit + * @return {GradleRelease} + */ +function getLatestRelease(releases, majorVersionLimit) { + const limitedRelease = releases + .sort((a, b) => compareVersionStrings(b.versionName, a.versionName)) + .find((release) => compareVersionStrings(release.versionName, majorVersionLimit) <= 0); + if (!limitedRelease) { + throw new Error(`No release found for major version limit: "${majorVersionLimit}"`); + } + return limitedRelease; +} + +/** + * @param {string} latestGradleVersion + * @return {string} + */ +function getLatestGradleUrl(latestGradleVersion) { + const format = config.format ?? 'bin'; + return `${URL_PREFIX}/gradle-${latestGradleVersion}-${format}.zip`; +} + +/** + * @param {Object} data + * @param {string} data.latestGradleVersion + * @param {string} data.latestGradleUrl + * @param {string} data.majorVersionLimit + * @return {Promise} + */ +async function updateGradleWrapperFileContent({ latestGradleVersion, latestGradleUrl, majorVersionLimit }) { + const path = '../gradle/wrapper/gradle-wrapper.properties'; + const messages = []; + const props = readPropertiesSync(path); + let propUrl = props[KEY]; + if (!propUrl) { + propUrl = latestGradleUrl; + messages.push(`Append: ${KEY}=${propUrl}`); + } + const re = /(gradle-)(.+)(-(?:bin|all)\.zip)/; + let propVersion = propUrl.match(re)?.[2]; + if (!propVersion) { + messages.push(`-- ${propUrl}\n-> ${latestGradleUrl}`); + propUrl = latestGradleUrl; + propVersion = latestGradleVersion; + } + + if (compareVersionStrings(propVersion, majorVersionLimit) > 0) { + const suffix = ` (降级, 受限于 "${config.majorVersionLimit}")`; + messages.push(`-- ${propUrl}\n-> ${latestGradleUrl}${suffix}`); + props[KEY] = propUrl.replace(re, `$1${latestGradleVersion}$3`); + } else if (compareVersionStrings(propVersion, latestGradleVersion) < 0) { + const suffix = isVersionLimited() ? ` (升级, 但受限于 "${config.majorVersionLimit}")` : ` (升级)`; + messages.push(`-- ${propUrl}\n-> ${latestGradleUrl}${suffix}`); + props[KEY] = propUrl.replace(re, `$1${latestGradleVersion}$3`); + } + + if (messages.length > 0) { + writePropertiesSync(path, props); + console.log('[gradle-wrapper.properties] 已更新 (Gradle 版本)'); + messages.forEach((message) => console.log(message)); + } else { + // console.log('[gradle-wrapper.properties] 无需更新 (Gradle 版本)'); + } +} + +function parseMajorVersionLimit() { + const majorVersionLimit = config.majorVersionLimit ?? 'x.x.x'; + const limits = String(majorVersionLimit).split('.'); + for (let i = 0; i < limits.length; i++) { + let limit = limits[i]; + if (limit.match(/([?x])(\.\1)*/i)) { + limits[i] = limit = '9'.repeat(9); + } + if (isNaN(parseInt(limit))) { + throw new Error(`Invalid major version limit: ${majorVersionLimit}`); + } + } + return limits.join('.'); +} + +(async function main() { + const releases = await fetchGradleReleases(); + if (!releases || releases.length === 0) { + throw new Error('No Gradle releases found'); + } + const majorVersionLimit = parseMajorVersionLimit(); + const latestRelease = getLatestRelease(releases, majorVersionLimit); + const latestGradleVersion = latestRelease.versionName; + const latestGradleUrl = getLatestGradleUrl(latestGradleVersion); + await updateGradleWrapperFileContent({ + latestGradleVersion, latestGradleUrl, majorVersionLimit, + }); +})().catch((e) => { + console.error('Failed to scrape or inject latest Gradle wrapper:', e); + process.exit(1); +}); \ No newline at end of file diff --git a/.utils/utils/date.mjs b/.utils/utils/date.mjs index ffd0b192..1d8a3d23 100644 --- a/.utils/utils/date.mjs +++ b/.utils/utils/date.mjs @@ -20,4 +20,34 @@ export function toYYYYMMDD(dateText = '') { const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${y}/${m}/${day}`; -} \ No newline at end of file +} + +/** + * 生成 .properties 文件头部注释时间戳. + * 形如: "#Thu Aug 28 12:05:55 CST 2025" (Properties.store 风格, en-US + short tz) + * 注: 时区缩写依赖运行环境, 可能显示为 GMT+08/PDT 等. + * @param {Date} [date=new Date()] + * @param {string} [timeZone="Asia/Shanghai"] 可选时区, 如 "Asia/Shanghai" + * @returns {string} + */ +export function generatePropertiesFileTimestamp(date = new Date(), timeZone) { + const fmt = new Intl.DateTimeFormat('en-US', { + weekday: 'short', + month: 'short', + day: '2-digit', + year: 'numeric', + hour: '2-digit', + minute: '2-digit', + second: '2-digit', + hour12: false, + timeZoneName: 'short', + ...(timeZone ? { timeZone } : { timeZone: 'Asia/Shanghai' }), + }); + const parts = fmt.formatToParts(date).reduce((acc, p) => { + acc[p.type] = p.value; + return acc; + }, /** @type {Object} */ ({})); + // parts 示例: { weekday: 'Thu', month:'Aug', day:'28', hour:'12', minute:'05', second:'55', timeZoneName:'CST', year:'2025' } + const stamp = `${parts.weekday} ${parts.month} ${parts.day} ${parts.hour}:${parts.minute}:${parts.second} ${parts.timeZoneName} ${parts.year}`; + return `#${stamp}`; +} diff --git a/.utils/utils/properties.mjs b/.utils/utils/properties.mjs index 92bad626..614b6bcb 100644 --- a/.utils/utils/properties.mjs +++ b/.utils/utils/properties.mjs @@ -3,6 +3,7 @@ import * as fs from 'node:fs'; import * as fsp from 'node:fs/promises'; import { compareVersionStrings } from './versioning.mjs'; +import { generatePropertiesFileTimestamp } from './date.mjs'; /** * @param {string} str @@ -55,6 +56,80 @@ function unescapeProperty(str) { return out; } +/** + * 将 JS 字符串按 .properties 规范转义 (store 格式) + * - 转义: backslash, 空白/控制字符, 分隔符 (= :), 注释首字符 (# !) + * - 非 ASCII 均编码为 \uXXXX (与 java.util.Properties.store 对齐) + * @param {string} str + * @param {boolean} isKey + * @returns {string} + */ +function escapeProperty(str, isKey) { + if (!str) return ''; + let out = ''; + for (let i = 0; i < str.length; i++) { + const ch = str[i]; + const code = ch.codePointAt(0); + // 统一处理非 ASCII 或控制字符 + if (code < 0x20 || code > 0x7e) { + if (ch === '\t') { + out += '\\t'; + continue; + } + if (ch === '\n') { + out += '\\n'; + continue; + } + if (ch === '\r') { + out += '\\r'; + continue; + } + if (ch === '\f') { + out += '\\f'; + continue; + } + // 其他非 ASCII -> \uXXXX + const hex = code.toString(16).padStart(4, '0'); + out += '\\u' + hex.slice(-4); + continue; + } + switch (ch) { + case '\\': + out += '\\\\'; + break; + case '=': + case ':': + // 在 key 和 value 中都转义, 保证兼容性 + out += '\\' + ch; + break; + case ' ': + // key 中任意空格需要转义; value 的前导空格需要转义 + if (isKey || out === '') out += '\\ '; + else out += ' '; + break; + case '\t': + case '\n': + case '\r': + case '\f': + // 已在上方控制字符分支处理, 这里冗余保护 + out += ch === '\t' ? '\\t' + : ch === '\n' ? '\\n' + : ch === '\r' ? '\\r' + : '\\f'; + break; + case '#': + case '!': + // 作为 key 时或 value 的首字符, 为避免被解析为注释, 需转义 + if (isKey || out === '') out += '\\' + ch; + else out += ch; + break; + default: + out += ch; + } + } + return out; +} + /** * @param {string} text * @returns {Object} @@ -158,6 +233,48 @@ export function readPropertiesSync(filePath = '../version.properties', { encodin return parseProperties(text); } +/** + * @param {string} [filePath='../version.properties']] + * @param {Object} [props={}] + * @param {Object} options + * @param {BufferEncoding} [options.encoding='utf8'] + * @return {Promise} + */ +export async function writeProperties(filePath = '../version.properties', props = {}, { encoding = 'utf8' } = {}) { + const lines = []; + for (const key in props) { + const value = props[key]; + if (value == null) continue; + const k = escapeProperty(String(key), true); + const v = escapeProperty(String(value), false); + lines.push(`${k}=${v}`); + } + lines.unshift(generatePropertiesFileTimestamp()); + const text = lines.join('\n') + '\n'; + return fsp.writeFile(filePath, text, { encoding }); +} + +/** + * @param {string} [filePath='../version.properties']] + * @param {Object} [props={}] + * @param {Object} options + * @param {BufferEncoding} [options.encoding='utf8'] + * @return {void} + */ +export function writePropertiesSync(filePath = '../version.properties', props = {}, { encoding = 'utf8' } = {}) { + const lines = []; + for (const key in props) { + const value = props[key]; + if (value == null) continue; + const k = escapeProperty(String(key), true); + const v = escapeProperty(String(value), false); + lines.push(`${k}=${v}`); + } + lines.unshift(generatePropertiesFileTimestamp()); + const text = lines.join('\n') + '\n'; + return fs.writeFileSync(filePath, text, { encoding }); +} + /** * @param {string} [filePath='../version.properties'] * @param {Object} options diff --git a/version.properties b/version.properties index 94e2aefe..92d360d6 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Wed Sep 10 13:18:36 CST 2025 -BUILD_TIME=1757481516029 +#Wed Sep 10 17:07:27 CST 2025 +BUILD_TIME=1757495247304 COMPILE_SDK_VERSION=35 IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_NDK_VERSION=26.1.10909125 @@ -24,6 +24,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 TARGET_SDK_VERSION=35 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=3360 +VERSION_BUILD=3361 VERSION_NAME=6.7.0 Alpha5 VSCODE_EXT_REQUIRED_VERSION=1.0.8