6.7.0 - Alpha5 - 新增 Gradle 版本 scraper 脚本
This commit is contained in:
75
.utils/fetch-and-parse-gradle-releases.mjs
Normal file
75
.utils/fetch-and-parse-gradle-releases.mjs
Normal 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;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ const __dirname = path.dirname(__filename);
|
|||||||
|
|
||||||
// 待执行脚本 (顺序可按需调整)
|
// 待执行脚本 (顺序可按需调整)
|
||||||
const SCRIPT_LIST = [
|
const SCRIPT_LIST = [
|
||||||
|
'scrape-and-inject-latest-gradle-wrapper.mjs',
|
||||||
'scrape-and-inject-agp-releases.mjs',
|
'scrape-and-inject-agp-releases.mjs',
|
||||||
'scrape-and-inject-android-studio-agp-version-map.mjs',
|
'scrape-and-inject-android-studio-agp-version-map.mjs',
|
||||||
'scrape-and-inject-android-studio-codename_maps.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>}
|
* @return {Promise<boolean>}
|
||||||
*/
|
*/
|
||||||
async function ensureExists(filePath) {
|
async function ensureExists(filePath) {
|
||||||
|
|||||||
138
.utils/scrape-and-inject-latest-gradle-wrapper.mjs
Normal file
138
.utils/scrape-and-inject-latest-gradle-wrapper.mjs
Normal 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);
|
||||||
|
});
|
||||||
@@ -20,4 +20,34 @@ export function toYYYYMMDD(dateText = '') {
|
|||||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||||
const day = String(d.getDate()).padStart(2, '0');
|
const day = String(d.getDate()).padStart(2, '0');
|
||||||
return `${y}/${m}/${day}`;
|
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}`;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as fsp from 'node:fs/promises';
|
import * as fsp from 'node:fs/promises';
|
||||||
import { compareVersionStrings } from './versioning.mjs';
|
import { compareVersionStrings } from './versioning.mjs';
|
||||||
|
import { generatePropertiesFileTimestamp } from './date.mjs';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} str
|
* @param {string} str
|
||||||
@@ -55,6 +56,80 @@ function unescapeProperty(str) {
|
|||||||
return out;
|
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
|
* @param {string} text
|
||||||
* @returns {Object<string, string>}
|
* @returns {Object<string, string>}
|
||||||
@@ -158,6 +233,48 @@ export function readPropertiesSync(filePath = '../version.properties', { encodin
|
|||||||
return parseProperties(text);
|
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 {string} [filePath='../version.properties']
|
||||||
* @param {Object} options
|
* @param {Object} options
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#Wed Sep 10 13:18:36 CST 2025
|
#Wed Sep 10 17:07:27 CST 2025
|
||||||
BUILD_TIME=1757481516029
|
BUILD_TIME=1757495247304
|
||||||
COMPILE_SDK_VERSION=35
|
COMPILE_SDK_VERSION=35
|
||||||
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
IMAGE_QUANT_CMAKE_VERSION=3.22.1
|
||||||
IMAGE_QUANT_NDK_VERSION=26.1.10909125
|
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
|
RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3
|
||||||
TARGET_SDK_VERSION=35
|
TARGET_SDK_VERSION=35
|
||||||
TARGET_SDK_VERSION_INRT=29
|
TARGET_SDK_VERSION_INRT=29
|
||||||
VERSION_BUILD=3360
|
VERSION_BUILD=3361
|
||||||
VERSION_NAME=6.7.0 Alpha5
|
VERSION_NAME=6.7.0 Alpha5
|
||||||
VSCODE_EXT_REQUIRED_VERSION=1.0.8
|
VSCODE_EXT_REQUIRED_VERSION=1.0.8
|
||||||
|
|||||||
Reference in New Issue
Block a user