6.7.0 - Alpha5 - 新增 Gradle 版本 scraper 脚本

This commit is contained in:
SuperMonster003
2025-09-10 19:40:51 +08:00
parent 881e03b866
commit 685fc46850
6 changed files with 366 additions and 5 deletions

View File

@@ -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<GradleRelease[]>}
*/
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;
});
}

View File

@@ -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<boolean>}
*/
async function ensureExists(filePath) {

View File

@@ -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<void>}
*/
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);
});

View File

@@ -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}`;
}
}
/**
* 生成 .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<string, string>} */ ({}));
// 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}`;
}

View File

@@ -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<string, string>}
@@ -158,6 +233,48 @@ export function readPropertiesSync(filePath = '../version.properties', { encodin
return parseProperties(text);
}
/**
* @param {string} [filePath='../version.properties']]
* @param {Object<string,string>} [props={}]
* @param {Object} options
* @param {BufferEncoding} [options.encoding='utf8']
* @return {Promise<void>}
*/
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<string,string>} [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

View File

@@ -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