6.7.0 - Alpha6 - 使用 Toolchain 替代 sourceCompatibility/targetCompatibility 以降低构建环境差异

This commit is contained in:
SuperMonster003
2025-09-24 16:47:39 +08:00
parent 4778794e0e
commit 2309377f97
18 changed files with 361 additions and 228 deletions

View File

@@ -37,7 +37,8 @@
"应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_", "应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_",
"Gradle 构建脚本提升 7z 格式文件的解压效率", "Gradle 构建脚本提升 7z 格式文件的解压效率",
"使用版本目录 (Version Catalogs) 集中管理 Gradle 依赖和插件版本", "使用版本目录 (Version Catalogs) 集中管理 Gradle 依赖和插件版本",
"模块化 Gradle 脚本, 将共享构建逻辑迁移至 buildSrc 并抽象为约定插件" "模块化 Gradle 脚本, 将共享构建逻辑迁移至 buildSrc 并抽象为约定插件",
"使用 Toolchain 替代 sourceCompatibility/targetCompatibility 以降低构建环境差异"
], ],
"dependency": [ "dependency": [
"本地化 Root Shell 版本 1.6", "本地化 Root Shell 版本 1.6",

View File

@@ -34,7 +34,8 @@ const childProcessOutput = [];
* @returns {string} * @returns {string}
*/ */
function getScriptProgressMessage(scripts, idx) { function getScriptProgressMessage(scripts, idx) {
return `[${idx + 1}/${scripts.length}] ${scripts[idx].name}`; const order = `${idx + 1}`.padStart(`${scripts.length}`.length, '0');
return `[${order}/${scripts.length}] ${scripts[idx].name}`;
} }
/** /**

View File

@@ -2,39 +2,70 @@
/** @typedef {import('puppeteer').Page} Page */ /** @typedef {import('puppeteer').Page} Page */
/** @typedef {import('puppeteer').Frame} Frame */ /** @typedef {import('puppeteer').Frame} Frame */
/** @typedef {import('./fetch-and-parse-android-studio-latest-stable-version.mjs').Row} Row */ /** @typedef {import('./fetch-and-parse-android-studio-latest-stable-version.mjs').StableArchiveItem} Row */
/** @typedef {import('./fetch-and-parse-android-studio-archives.mjs').ArchiveItem} ArchiveItem */ /** @typedef {import('./fetch-and-parse-android-studio-archives.mjs').ArchiveItem} ArchiveItem */
/** /**
* @template {Node} T * @template {Node} T
* @typedef {import('puppeteer').ElementHandle<T>} ElementHandle * @typedef {import('puppeteer').ElementHandle<T>} ElementHandle
*/ */
import { getLatestStableWindows } from './fetch-and-parse-android-studio-latest-stable-version.mjs'; import * as fsp from 'node:fs/promises';
import * as path from 'node:path';
import { batchUpdateAnchoredBlocks } from './utils/anchors.mjs'; import { batchUpdateAnchoredBlocks } from './utils/anchors.mjs';
import { compareVersionStrings } from './utils/versioning.mjs';
import { toUpdatedStamp, toYYYYMMDD } from './utils/date.mjs';
import { bytes2GiB } from './utils/format.mjs'; import { bytes2GiB } from './utils/format.mjs';
import { getRemoteFileSizeBytes } from './utils/fetch.mjs'; import { compareVersionStrings } from './utils/versioning.mjs';
import { getAndroidStudioArchives } from './fetch-and-parse-android-studio-archives.mjs'; import { getAndroidStudioArchives } from './fetch-and-parse-android-studio-archives.mjs';
import { getLatestStableArchives } from './fetch-and-parse-android-studio-latest-stable-version.mjs';
import { getRemoteFileSizeBytes } from './utils/fetch.mjs';
import { toUpdatedStamp, toYYYYMMDD } from './utils/date.mjs';
async function main() { /**
// 1) 用 "最新稳定版" 的校验和/文件名, 在归档中定位条目, 补全并更新 common.json * Manual override for codename mappings (for resolving codename prefix conflicts).<br>
* Default is first letter, e.g. 'Meerkat' and 'Bumblebee' gives { Meerkat: 'M', Bumblebee: 'B' }.<br>
* When first letters conflict, conflicts are auto-resolved,
* e.g. 'Camel' and 'Catfish' gives { Camel: 'CAM', Catfish: 'CAT' }.<br>
* For extreme cases where conflicts cannot be auto-resolved,
* e.g. 'Cat' and 'Catfish', manual prefix mapping is needed, like { Cat: 'CT', Catfish: 'CTF' }.<br>
* zh-CN:<br>
* 手动覆盖代号映射 (可用于解决代号前缀冲突).<br>
* 默认为首字母, 如 'Meerkat' 与 'Bumblebee', 得到 { Meerkat: 'M', Bumblebee: 'B' }.<br>
* 首字母重复时自动消解冲突, 如 'Camel' 与 'Catfish', 得到 { Camel: 'CAM', Catfish: 'CAT' }.<br>
* 极端情况无法自动消解冲突, 如 'Cat' 与 'Catfish', 此时需要手动指定前缀, 如 { Cat: 'CT', Catfish: 'CTF' }.
* @example Object<codename name, codename prefix>
* {
* 'Camel': 'CM',
* 'Cat': 'CT',
* 'Catfish': 'CTF',
* ... ...
* }
* @type {Object<string, string>}
*/
const manualCodenameOverrides = {};
const archives = await getAndroidStudioArchives(); /**
const latestRows = await getLatestStableWindows(); // [{kind, filename, sha256, url, size, ...}] * Use the latest stable version's checksum/filename to locate the entry in archives,
* complete and update common.json.<br>
* zh-CN: 用 "最新稳定版" 的校验和/文件名, 在归档中定位条目, 补全并更新 common.json.
*
* @param {ArchiveItem[]} archives
* @returns {Promise<void>}
*/
async function updateLatestArchiveInfo(archives) {
const latestRows = await getLatestStableArchives(); // [ { kind, filename, sha256, url, size, ... } ]
const latestExe = latestRows.find(x => x.kind === 'exe'); const latestExe = latestRows.find(x => x.kind === 'exe');
const latestZip = latestRows.find(x => x.kind === 'zip'); const latestZip = latestRows.find(x => x.kind === 'zip');
const latestTar = latestRows.find(x => x.kind === 'tar'); const latestTar = latestRows.find(x => x.kind === 'tar');
if (!latestExe || !latestZip || !latestTar) { if (!latestExe || !latestZip || !latestTar) {
throw new Error('最新稳定版条目缺少 Windows EXE/ZIP 或 TAR'); throw new Error('Latest stable archives missing required "kind" info: exe, zip, or tar');
} }
/** /**
* 在归档中查找: 优先用 sha256 命中, 其次用文件名. * Search the archive: first try to match by sha256, then by filename.<br>
* zh-CN: 在归档中查找: 优先用 sha256 命中, 其次用文件名.
* *
* @param {ArchiveItem[]} rows * @param {ArchiveItem[]} rows
* @param {Row} target * @param {Row} target
* @return {ArchiveItem | null} * @returns {ArchiveItem | null}
*/ */
const matchArchive = (rows, target) => { const matchArchive = (rows, target) => {
for (const arc of rows) { for (const arc of rows) {
@@ -46,29 +77,29 @@ async function main() {
}; };
const matchedArc = matchArchive(archives, latestExe) || matchArchive(archives, latestZip); const matchedArc = matchArchive(archives, latestExe) || matchArchive(archives, latestZip);
if (!matchedArc) { if (!matchedArc) {
throw new Error('未能在归档中定位到与最新版本对应的条目 (按 sha256/文件名均未命中)'); throw new Error('Could not locate entries in the archive corresponding to the latest version (neither by sha256 nor filename)');
} }
/** /**
* 从匹配到的 expandable 中抽取 Windows EXE/ZIP 的链接/文件名.
*
* @param {string} suffix * @param {string} suffix
* @return {{ filename: string, url: string, sizeGiB?: string | null } | null} * @returns {{ filename: string, url: string, sizeGiB?: string | null } | null}
*/ */
const pickWinItem = suffix => { const pickWinItem = (suffix) => {
// suffix: "-windows.exe" | "-windows.zip"
const link = matchedArc.links.find(l => l.text.endsWith(suffix)); const link = matchedArc.links.find(l => l.text.endsWith(suffix));
if (!link) return null; if (!link) return null;
return { return { filename: link.text, url: link.href };
filename: link.text,
url: link.href,
};
}; };
const exeItem = pickWinItem('-windows.exe'); const exeItem = pickWinItem('-windows.exe');
const zipItem = pickWinItem('-windows.zip'); const zipItem = pickWinItem('-windows.zip');
const tarItem = pickWinItem('-linux.tar.gz'); const tarItem = pickWinItem('-linux.tar.gz');
// 查询真实文件大小 (并发获取), 格式化为 GiB if (!exeItem || !zipItem || !tarItem) {
throw new Error('Matched archive entry missing Windows EXE/ZIP or Linux TAR download information');
}
// Query real file size (concurrent fetch) and format to GiB.
// zh-CN: 查询真实文件大小 (并发获取), 格式化为 GiB.
const [ exeBytes, zipBytes, tarBytes ] = await Promise.all([ const [ exeBytes, zipBytes, tarBytes ] = await Promise.all([
exeItem ? getRemoteFileSizeBytes(exeItem.url) : Promise.resolve(null), exeItem ? getRemoteFileSizeBytes(exeItem.url) : Promise.resolve(null),
zipItem ? getRemoteFileSizeBytes(zipItem.url) : Promise.resolve(null), zipItem ? getRemoteFileSizeBytes(zipItem.url) : Promise.resolve(null),
@@ -78,20 +109,17 @@ async function main() {
if (zipItem) zipItem.sizeGiB = bytes2GiB(zipBytes); if (zipItem) zipItem.sizeGiB = bytes2GiB(zipBytes);
if (tarItem) tarItem.sizeGiB = bytes2GiB(tarBytes); if (tarItem) tarItem.sizeGiB = bytes2GiB(tarBytes);
// 准备写回 common.json 所需字段 // Prepare fields needed to write back to common.json.
const latestVersionName = matchedArc.title.trim(); // 例: "Android Studio Narwhal Feature Drop | 2025.1.2" // zh-CN: 准备写回 common.json 所需字段.
const latestVersionDate = toYYYYMMDD(matchedArc.date) || ''; // 例: "2025/07/31"
if (!exeItem || !zipItem) { const latestVersionName = matchedArc.title.trim(); // e.g. "Android Studio Narwhal Feature Drop | 2025.1.2"
throw new Error('匹配到的归档条目缺少 Windows EXE 或 ZIP 下载信息'); const latestVersionDate = toYYYYMMDD(matchedArc.date) || ''; // e.g. "2025/07/31"
}
// 读取并更新 common.json (仅更新带有 "android_studio_latest_" 片段的键与版本名日期键) // Read and update common.json (only update keys containing "android_studio_latest_" fragment and version name date key).
const fs = await import('node:fs/promises'); // zh-CN: 读取并更新 common.json (仅更新带有 "android_studio_latest_" 片段的键与版本名日期键).
const path = await import('node:path');
const commonJsonPath = path.resolve(process.cwd(), '../.readme/common.json'); const commonJsonPath = path.resolve(process.cwd(), '../.readme/common.json');
const commonRaw = await fs.readFile(commonJsonPath, 'utf8'); const commonRaw = await fsp.readFile(commonJsonPath, 'utf8');
const commonObj = JSON.parse(commonRaw); const commonObj = JSON.parse(commonRaw);
const updatedCommon = { const updatedCommon = {
@@ -110,60 +138,112 @@ async function main() {
}; };
if (JSON.stringify(updatedCommon) !== JSON.stringify(commonObj)) { if (JSON.stringify(updatedCommon) !== JSON.stringify(commonObj)) {
await fs.writeFile(commonJsonPath, JSON.stringify(updatedCommon, null, 2), 'utf8'); await fsp.writeFile(commonJsonPath, JSON.stringify(updatedCommon, null, 2), 'utf8');
console.log('[common.json] 已更新 (Android Studio 数据)'); console.log('[common.json] Updated (Android Studio information)');
console.log(`-- '${commonObj.android_studio_latest_recommended_version_name}'`); if (commonObj.android_studio_latest_recommended_version_name !== updatedCommon.android_studio_latest_recommended_version_name) {
console.log(`-> '${updatedCommon.android_studio_latest_recommended_version_name}'`); console.log(`-- '${commonObj.android_studio_latest_recommended_version_name}'`);
console.log(`-> '${updatedCommon.android_studio_latest_recommended_version_name}'`);
}
} else { } else {
// console.log('[common.json] 无需更新 (Android Studio 数据)'); // console.log('[common.json] No update needed (Android Studio information)');
} }
}
/**
* Summarize codename-to-version mappings and codename first release dates.<br>
* zh-CN: 汇总代号版本映射以及代号的首发日期.
*
* @param {ArchiveItem[]} archives
*/
function getCodenameMapLinesInfo(archives) {
// 2) 汇总代号 - 版本映射与代号首发日期, 更新 settings.gradle.kts 两个锚点块
// 2.1 从标题中解析 "代号与版本号"
// 标题模式示例: "Android Studio Meerkat Feature Drop | 2024.3.2 RC 1"
/** /**
* @param {string} t * Parse "codename and version number" from title,
* @return {string | null} * zh-CN: 从标题中解析 "代号与版本号",
*
* @example string
* 'Android Studio Meerkat Feature Drop | 2024.3.2 RC 1'
*
* @param {string} title
* @returns {string | null}
*/ */
const codenameFromTitle = t => { const codenameFromTitle = title => {
// 捕获 "Android Studio <Codename> [Feature Drop] |" // Capture "Android Studio <Codename> [Feature Drop]".
const m = /Android Studio\s+(.+?)\s*(?:(\s+\d+\s+)?Feature Drop)?\s*\|/i.exec(t); // zh-CN: 捕获 "Android Studio <Codename> [Feature Drop]".
const m = /Android Studio\s+(.+?)\s*(?:(\s+\d+\s+)?Feature Drop)?(?=\s*\|)/i.exec(title);
return m ? m[1].trim() : null; return m ? m[1].trim() : null;
}; };
// 用户手动覆盖映射 (可按需填写或从外部读取)
/** @type {Object<string, string>} */
const manualCodenameOverrides = {
/* e.g. 'Meerkat': 'Mkt' */
};
// 生成唯一代码: 按顺序, 出现冲突则对冲突组统一递增长度; 覆盖项优先生效且不可被自动改动
/** /**
* @param {string[]} names 原始代号 (保留大小写与空格, 顺序与站点一致) * Generate unique codes: sequentially, uniformly increase length for conflict groups;
* @param {Object<string, string>} overrides 手动覆盖映射 * override mappings take precedence.<br>
* @returns {Map<string, string>} name -> code * zh-CN:<br>
* 生成唯一代码: 按顺序, 出现冲突则对冲突组统一递增长度; 覆盖映射优先考虑.
*
* @example Map<name, prefixCode>
* Map(14) {
* 'Arctic Fox' => 'A',
* 'Bumblebee' => 'B',
* ... ...
* 'Narwhal' => 'N'
* }
*
* @param {string[]} names
* Original codenames (preserves case and spaces, e.g. "Arctic Fox", "Bumblebee", "Narwhal" etc).<br>
* zh-CN: 原始代号 (保留大小写与空格, 如 "Arctic Fox", "Bumblebee", "Narwhal" 等).
* @param {Object<string, string>} overrides
* Manual override mappings.<br>
* zh-CN: 手动覆盖映射.
* @returns {Map<string, string>}
*/ */
function buildUniquePrefixes(names, overrides) { const buildUniquePrefixes = (names, overrides) => {
// 代码不包含空格; 按名称去空格后逐字符递增长度
const entries = names.map(n => ({
name: n,
base: n.replace(/\s+/g, ''), // 去空格用于截取
len: Math.max(1, overrides[n] ? overrides[n].replace(/\s+/g, '').length : 1),
code: overrides[n] ? overrides[n].replace(/\s+/g, '') : null,
locked: !!overrides[n],
}));
// 先赋初值 /**
for (const e of entries) { * @param {string} s
if (!e.code) e.code = e.base.slice(0, e.len); * @returns {string}
} */
const normalize = (s) => s.replace(/\s+/g, '');
// 检测并解决冲突 /**
* @param {string} s
* @param {string} def
* @returns {string}
*/
const normalizeOrDefault = (s, def) => s && normalize(s) || normalize(def);
/**
* @param {string }s
* @param {number} def
* @returns {number}
*/
const normalizeLength = (s, def) => s && normalize(s).length || def;
const entries = names.map(name => {
const override = overrides[name]?.toUpperCase();
const base = normalize(name);
const len = normalizeLength(override, 1);
const code = normalizeOrDefault(override, base.slice(0, len)).toUpperCase();
const locked = Boolean(override);
return { name, base, len, code, locked };
});
// Detect and resolve conflicts. (zh-CN: 检测并解决冲突.)
const maxLenByName = new Map(entries.map(e => [ e.name, e.base.length ])); const maxLenByName = new Map(entries.map(e => [ e.name, e.base.length ]));
// 循环上限保护, 防止极端情况下死循环 // Loop limit protection to prevent infinite loops in extreme cases.
for (let step = 0; step < 1024; step++) { // zh-CN: 循环上限保护, 防止极端情况下死循环.
// 统计冲突组: code -> indices for (let step = 0; step < 1 << 10; step++) {
/** @type {Map<string, number[]>} */ /**
* Count conflict groups. (zh-CN: 统计冲突组.)
* @example Map<code, indices>
* Map(14) {
* 'N' => [ 0 ],
* 'M' => [ 1 ],
* ... ...
* 'B' => [ 12 ],
* 'A' => [ 13 ]
* }
* @type {Map<string, number[]>}
*/
const bucket = new Map(); const bucket = new Map();
entries.forEach((e, idx) => { entries.forEach((e, idx) => {
const key = e.code; const key = e.code;
@@ -171,30 +251,26 @@ async function main() {
bucket.get(key).push(idx); bucket.get(key).push(idx);
}); });
// 找到有冲突的组 (size >= 2) const conflicts = Array.from(bucket.entries()).filter(([ _, indices ]) => indices.length >= 2);
const conflicts = Array.from(bucket.entries()).filter(([ , indices ]) => indices.length >= 2); if (conflicts.length === 0) {
if (conflicts.length === 0) break; // 已无冲突 // No conflicts, or all conflicts have been resolved.
// zh-CN: 没有冲突, 或冲突已全部解决.
break;
}
// 逐组处理
for (const [ code, indices ] of conflicts) { for (const [ code, indices ] of conflicts) {
// 若组内存在 >=2 个锁定项且 code 相同 -> 直接报错
const lockedIndices = indices.filter(i => entries[i].locked); const lockedIndices = indices.filter(i => entries[i].locked);
if (lockedIndices.length >= 2) { if (lockedIndices.length >= 2) {
const letter = code[0]?.toUpperCase() || '?';
const groupNames = indices.map(i => entries[i].name); const groupNames = indices.map(i => entries[i].name);
throw new Error(`[CodenameMap] 手动覆盖映射发生冲突: "${code}" -> ${groupNames.join(', ')}. 请调整覆盖映射. 冲突首字母组: ${letter}*`); throw new Error(`[CodenameMap] Manual override mapping conflict: "${code}" -> [ ${groupNames.join(', ')} ]`);
} }
// 让组内所有 "未锁定项" 统一递增长度
for (const i of indices) { for (const i of indices) {
const e = entries[i]; const e = entries[i];
if (e.locked) continue; // 覆盖项不改 if (e.locked) continue;
const maxLen = maxLenByName.get(e.name); const maxLen = maxLenByName.get(e.name);
if (e.len >= maxLen) { if (e.len >= maxLen) {
// 已经用到全长仍冲突 -> 无法自动消解 const groupNames = indices.map(i => entries[i].name);
const letter = e.base[0]?.toUpperCase() || '?'; throw new Error(`[CodenameMap] Unable to auto-resolve conflicts: [ ${groupNames.join(', ')} ]`);
const groupNames = indices.map(ii => entries[ii].name);
throw new Error(`[CodenameMap] 无法自动消解冲突 ("${e.name}" 与同组名称完全相同或为彼此前缀到尽头). 请为该首字母组手动指定覆盖映射.\n- 组首字母: ${letter}\n- 组成员: ${groupNames.join(', ')}`);
} }
e.len += 1; e.len += 1;
e.code = e.base.slice(0, e.len); e.code = e.base.slice(0, e.len);
@@ -203,42 +279,52 @@ async function main() {
} }
return new Map(entries.map(e => [ e.name, e.code ])); return new Map(entries.map(e => [ e.name, e.code ]));
} };
// 提取按页面顺序的代号列表 (去重, 保留第一次出现顺序) const codenames = [ ...new Set(archives.map(o => codenameFromTitle(o.title)).filter(Boolean)) ];
const codenamesOrdered = []; const nameToCode = buildUniquePrefixes(codenames, manualCodenameOverrides);
const seen = new Set();
for (const arc of archives) {
const cname = codenameFromTitle(arc.title);
if (!cname) continue;
if (!seen.has(cname)) {
seen.add(cname);
codenamesOrdered.push(cname);
}
}
// 构建 name->code 映射 (按规则自动消解冲突; 支持手动覆盖) /**
const nameToCode = buildUniquePrefixes(codenamesOrdered, manualCodenameOverrides); * @example Map<version, Set<prefixCode>>
* Map(22) {
// 收集每版本 (yyyy.m.patch) 对应的代号集合, 以及每个代号的首发日期 * ... ...
/** @type {Map<string, Set<string>>} */ * '2024.2.1' => Set(1) { 'L' },
* '2024.1.3' => Set(1) { 'L' },
* '2024.1.2' => Set(1) { 'K' },
* '2024.1.1' => Set(1) { 'K' },
* '2023.3.2' => Set(2) { 'K', 'J' },
* '2023.3.1' => Set(1) { 'J' },
* '2023.2.1' => Set(1) { 'I' },
* ... ...
* }
* @type {Map<string, Set<string>>}
*/
const versionToLetters = new Map(); const versionToLetters = new Map();
/** @type {Map<string, { name:string, born: Date } >} */
/**
* @example Map<prefixCode, { name, born }>
* Map(14) {
* 'N' => { name: 'Narwhal', born: 2025-03-18T16:00:00.000Z },
* 'M' => { name: 'Meerkat', born: 2024-11-11T16:00:00.000Z },
* ... ...
* 'B' => { name: 'Bumblebee', born: 2021-05-17T16:00:00.000Z },
* 'A' => { name: 'Arctic Fox', born: 2021-01-25T16:00:00.000Z }
* }
* @type {Map<string, { name: string, born: Date } >}
*/
const letterBorn = new Map(); const letterBorn = new Map();
for (const arc of archives) { for (const arc of archives) {
if (!arc.version) continue; // 版本号 (yyyy.m.patch) if (!arc.version) continue;
const cname = codenameFromTitle(arc.title); const cname = codenameFromTitle(arc.title);
if (!cname) continue; if (!cname) continue;
const code = nameToCode.get(cname); const code = nameToCode.get(cname);
if (!code) continue; if (!code) continue;
// 映射 version -> codes
if (!versionToLetters.has(arc.version)) versionToLetters.set(arc.version, new Set()); if (!versionToLetters.has(arc.version)) versionToLetters.set(arc.version, new Set());
versionToLetters.get(arc.version).add(code); versionToLetters.get(arc.version).add(code);
// 记录代号首次出现日期
const d = new Date(arc.date); const d = new Date(arc.date);
const existed = letterBorn.get(code); const existed = letterBorn.get(code);
if (!existed || d < existed.born) { if (!existed || d < existed.born) {
@@ -246,40 +332,100 @@ async function main() {
} }
} }
// 生成 codenameVersionMap 文本 (按版本倒序排列, 值用 "A|B" 连接)
const sortedVersions = Array.from(versionToLetters.keys()).sort((a, b) => { const sortedVersions = Array.from(versionToLetters.keys()).sort((a, b) => {
// Split version string into [ y: year, m: minor, p: patch ].
// zh-CN: 将版本字符串拆分为 [ y: 年份, m: 次版本号, p: 补丁版本号 ].
// e.g. "2024.3.2" -> [ y: 2024, m: 3, p: 2 ].
const [ ay, am, ap ] = a.split('.').map(Number); const [ ay, am, ap ] = a.split('.').map(Number);
const [ by, bm, bp ] = b.split('.').map(Number); const [ by, bm, bp ] = b.split('.').map(Number);
return by - ay || bm - am || bp - ap; return by - ay || bm - am || bp - ap;
}); });
/**
* @example Array<[version, jointPrefixCode]>
* [
* ... ..
* [ '2024.2.2', 'L' ],
* [ '2024.2.1', 'L' ],
* [ '2024.1.3', 'L' ],
* [ '2024.1.2', 'K' ],
* [ '2024.1.1', 'K' ],
* [ '2023.3.2', 'J|K' ],
* [ '2023.3.1', 'J' ],
* [ '2023.2.1', 'I' ],
* ... ...
* ]
* @type {Array<[string, string]>}
*/
const versionLettersList = sortedVersions.map(v => [ v, Array.from(versionToLetters.get(v)).sort().join('|') ]); const versionLettersList = sortedVersions.map(v => [ v, Array.from(versionToLetters.get(v)).sort().join('|') ]);
/* e.g. { "2023.3": {1: "J", 2: "J|K"} }. */ /**
/** @type {Object<string, { [patch: number]: string }>} */ * @example { minorSeries: { patch: letters } }
* {
* ... ...
* '2025.1': { '1': 'N', '2': 'N', '3': 'N', '4': 'N' },
* '2024.3': { '1': 'M', '2': 'M' },
* '2024.2': { '1': 'L', '2': 'L' },
* '2024.1': { '1': 'K', '2': 'K', '3': 'L' },
* '2023.3': { '1': 'J', '2': 'J|K' },
* '2023.2': { '1': 'I' },
* '2023.1': { '1': 'H' },
* '2022.3': { '1': 'G' },
* ... ...
* }
* @type {Object<string, { [patch: number]: string }>}
*/
const rawVersionLettersMap = {}; const rawVersionLettersMap = {};
for (let i = 0; i < versionLettersList.length; i++) { for (let i = 0; i < versionLettersList.length; i++) {
const [ v, letters ] = versionLettersList[i]; const [ v, letters ] = versionLettersList[i];
const matched = v.match(/(^\d+\.\d+)(?:\.(\d+))?/); const matched = v.match(/(^\d+\.\d+)(?:\.(\d+))?/);
if (!matched) continue; if (!matched) continue;
const [ , major, patch ] = matched; const [ , minorSeries, patch ] = matched;
if (major in rawVersionLettersMap) { if (minorSeries in rawVersionLettersMap) {
rawVersionLettersMap[major][patch] = letters; rawVersionLettersMap[minorSeries][patch] = letters;
} else { } else {
rawVersionLettersMap[major] = { [patch]: letters }; rawVersionLettersMap[minorSeries] = { [patch]: letters };
} }
} }
/* e.g. { "2024.3": "M", "2024.1.2": "K" }. */ /**
* When all versions with the same minor series prefix (like `2025.1.x`)
* point to the same codename prefix (like `'N'`),
* they can be merged (like `{ '2025.1' : 'N' }`, where `2025.1` is the minor series prefix),
* otherwise retain the original split form (like `2024.1.x` cannot be merged).<br>
* zh-CN:<br>
* 当次版本系列相同的版本 (如 `2025.1.x`) 全部指向同一个代号前缀 (如 `'N'`) 时,
* 可进行合并 (如 `{ '2025.1' : 'N' }`, 其中 `2025.1` 为次版本系列),
* 否则保留原始的拆分形式 (如 `2024.1.x` 不可合并).
* @example { version: letters }
* {
* ... ...
* '2025.1': 'N',
* '2024.3': 'M',
* '2024.2': 'L',
* '2024.1.1': 'K',
* '2024.1.2': 'K',
* '2024.1.3': 'L',
* '2023.3.1': 'J',
* '2023.3.2': 'J|K',
* '2023.2': 'I',
* '2023.1': 'H',
* '2022.3': 'G',
* ... ...
* }
* @type {Object<[version: string], string>}
*/
const combinedVersionLettersMap = {}; const combinedVersionLettersMap = {};
Object.entries(rawVersionLettersMap).forEach(([ major, patchToLetters ]) => { Object.entries(rawVersionLettersMap).forEach(([ minorSeries, patchToLetters ]) => {
const letterValues = Object.values(patchToLetters); const letterValues = Object.values(patchToLetters);
if (new Set(letterValues).size === 1) { if (new Set(letterValues).size === 1) {
combinedVersionLettersMap[major] = letterValues[0]; /* Combine. (zh-CN: 合并.) */
combinedVersionLettersMap[minorSeries] = letterValues[0];
} else { } else {
/* Keep expanded. (zh-CN: 保持展开.) */
Object.entries(patchToLetters).forEach(([ patch, letters ]) => { Object.entries(patchToLetters).forEach(([ patch, letters ]) => {
combinedVersionLettersMap[`${major}.${patch}`] = letters; combinedVersionLettersMap[`${minorSeries}.${patch}`] = letters;
}); });
} }
}); });
@@ -288,32 +434,37 @@ async function main() {
.sort((a, b) => compareVersionStrings(b[0], a[0])) .sort((a, b) => compareVersionStrings(b[0], a[0]))
.map(([ v, letters ]) => `"${v}" to "${letters}",`); .map(([ v, letters ]) => `"${v}" to "${letters}",`);
// 生成 codenameMap 文本 (按 born 日期倒序; 注释: Born on Mon d, yyyy.)
// 这里 key 为自动生成的 code (可能为一到多字符), value 为完整代号
const sortedLetters = Array.from(letterBorn.entries()) const sortedLetters = Array.from(letterBorn.entries())
.sort((a, b) => b[1].born.getTime() - a[1].born.getTime()); .sort((a, b) => b[1].born.getTime() - a[1].born.getTime());
const codenameMapLines = sortedLetters.map(([ code, { name, born } ]) => { const codenameMapLines = sortedLetters.map(([ code, { name, born } ]) => {
const bornStr = toUpdatedStamp(born); const bornStr = toUpdatedStamp(born);
// e.g. `"M" to "Meerkat", /* Born on Nov 12, 2024. */`.
return `"${code}" to "${name}", /* Born on ${bornStr}. */`; return `"${code}" to "${name}", /* Born on ${bornStr}. */`;
}); });
return { versionMapLines, codenameMapLines };
}
(async function main() {
const archives = await getAndroidStudioArchives();
await updateLatestArchiveInfo(archives);
const { versionMapLines, codenameMapLines } = getCodenameMapLinesInfo(archives);
await batchUpdateAnchoredBlocks('../settings.gradle.kts', [ { await batchUpdateAnchoredBlocks('../settings.gradle.kts', [ {
type: 'map', type: 'map',
anchorTag: 'ANDROID_STUDIO_CODENAME_VERSION_MAP', anchorTag: 'ANDROID_STUDIO_CODENAME_VERSION_MAP',
mapName: 'codenameVersionMap', mapName: 'codenameVersionMap',
lines: versionMapLines, lines: versionMapLines,
updatedLabel: 'Android Studio 代号版本映射', updatedLabel: 'Android Studio codename version map',
}, { }, {
type: 'map', type: 'map',
anchorTag: 'ANDROID_STUDIO_CODENAME_MAP', anchorTag: 'ANDROID_STUDIO_CODENAME_MAP',
mapName: 'codenameMap', mapName: 'codenameMap',
lines: codenameMapLines, lines: codenameMapLines,
updatedLabel: 'Android Studio 代号名称映射', updatedLabel: 'Android Studio codename map',
} ]); } ]);
} })().catch(err => {
main().catch(err => {
console.error(err); console.error(err);
process.exitCode = 1; process.exitCode = 1;
}); });

View File

@@ -10,6 +10,7 @@ plugins {
id("org.autojs.build.utils") id("org.autojs.build.utils")
id("org.autojs.build.versions") id("org.autojs.build.versions")
id("org.autojs.build.signs") id("org.autojs.build.signs")
id("org.autojs.build.jvm-convention")
id("com.android.application") id("com.android.application")
id("com.google.devtools.ksp") id("com.google.devtools.ksp")
id("org.jetbrains.kotlin.android") /* kotlin("android") */ id("org.jetbrains.kotlin.android") /* kotlin("android") */
@@ -567,8 +568,6 @@ android {
compileOptions { compileOptions {
isCoreLibraryDesugaringEnabled = true isCoreLibraryDesugaringEnabled = true
sourceCompatibility = versions.javaVersion
targetCompatibility = versions.javaVersion
} }
// @Hint by SuperMonster003 on Sep 25, 2024. // @Hint by SuperMonster003 on Sep 25, 2024.
@@ -619,12 +618,6 @@ android {
} }
} }
@Suppress("DEPRECATION")
kotlinOptions {
jvmTarget = versions.javaVersion.toString()
// freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
}
lint { lint {
abortOnError = false abortOnError = false
} }
@@ -745,12 +738,6 @@ tasks {
// options.compilerArgs.addAll(listOf("-Xlint:deprecation", "-Xlint:unchecked")) // options.compilerArgs.addAll(listOf("-Xlint:deprecation", "-Xlint:unchecked"))
} }
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
// @Archived by SuperMonster003 on Aug 14, 2024.
// # kotlinOptions { jvmTarget = versions.javaVersion.toString() }
compilerOptions { jvmTarget.set(JvmTarget.valueOf("JVM_${versions.javaVersion}")) }
}
register<Copy>("appendDigestToReleasedFiles") { register<Copy>("appendDigestToReleasedFiles") {
listOf(flavorNameApp, flavorNameInrt).forEach { flavorName -> listOf(flavorNameApp, flavorNameInrt).forEach { flavorName ->
val src = "$flavorName/$buildTypeRelease" val src = "$flavorName/$buildTypeRelease"

View File

@@ -52,6 +52,14 @@ gradlePlugin {
t.description = "Provides properties helpers." t.description = "Provides properties helpers."
} }
}) })
create("jvmConvention", object : Action<PluginDeclaration> {
override fun execute(t: PluginDeclaration) {
t.id = "org.autojs.build.jvm-convention"
t.implementationClass = "org.autojs.build.JvmConventionPlugin"
t.displayName = "AutoJs6 JVM Convention Plugin"
t.description = "Configures Java/Kotlin targets for Android modules using central Versions."
}
})
} }
} }

View File

@@ -17,3 +17,10 @@ dependencyResolutionManagement {
} }
} }
} }
plugins {
// @Hint by SuperMonster003 on Sep 14, 2025.
// ! Enable JDK auto-resolution/download capability for build modules.
// ! zh-CN: 让构建模块具备 JDK 自动解析/下载能力.
id("org.gradle.toolchains.foojay-resolver-convention")
}

View File

@@ -0,0 +1,32 @@
@file:Suppress("unused")
package org.autojs.build
import org.gradle.api.Plugin
import org.gradle.api.Project
/**
* Convention plugin: Unified Java/Kotlin target version configuration for Android modules.
*
* zh-CN: 约定插件: 为 Android 模块统一配置 Java/Kotlin 目标版本.
*
* - `id`: "org.autojs.build.jvm-convention"
* - `implementationClass`: "org.autojs.build.JvmConventionPlugin"
* - `displayName`: "AutoJs6 JVM Convention Plugin"
* - `description`: "Configures Java/Kotlin targets for Android modules using central Versions."
*
* Apply this plugin to your Android module's `build.gradle.kts`:
*
* zh-CN: 在 Android 模块的 `build.gradle.kts` 中应用此插件:<br>
*
* ```kts
* plugins {
* id("org.autojs.build.jvm-convention")
* }
* ```
*/
class JvmConventionPlugin : Plugin<Project> {
override fun apply(project: Project) {
Utils.configureJvmForAndroidModule(project)
}
}

View File

@@ -11,6 +11,10 @@
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true # org.gradle.parallel=true
#Thu Nov 11 16:38:18 CST 2021 #Thu Nov 11 16:38:18 CST 2021
# Configure memory settings for Gradle daemon and Kotlin daemon
# -Xms: Initial heap size
# -Xmx: Maximum heap size
# UseParallelGC: Use parallel garbage collector for better performance
org.gradle.jvmargs=-Xms4g -Xmx4g -Dkotlin.daemon.jvm.options\="-Xmx4g" -Dfile.encoding\=UTF-8 -XX:+UseParallelGC org.gradle.jvmargs=-Xms4g -Xmx4g -Dkotlin.daemon.jvm.options\="-Xmx4g" -Dfile.encoding\=UTF-8 -XX:+UseParallelGC
# https://docs.gradle.org/current/userguide/gradle_daemon.html # https://docs.gradle.org/current/userguide/gradle_daemon.html
org.gradle.daemon=true org.gradle.daemon=true
@@ -35,7 +39,13 @@ android.enableJetifier=true
# thereby reducing the size of the R class for that library # thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true android.nonTransitiveRClass=true
android.nonFinalResIds=true android.nonFinalResIds=true
# -- archived fields -- # Configure Java installation settings
# Added since Gradle Build Tool version 8.0.0-alpha09 # auto-detect: Automatically detect Java installations on the system
# Removed since Gradle Build Tool version 8.2.0-alpha15 # auto-download: Automatically download required Java version if not found
org.gradle.java.installations.auto-detect=true
org.gradle.java.installations.auto-download=true
# BuildConfig feature flag history:
# - Added in Gradle Build Tool 8.0.0-alpha09
# - Removed in Gradle Build Tool 8.2.0-alpha15
# - Controls generation of BuildConfig class
# android.defaults.buildfeatures.buildconfig=true # android.defaults.buildfeatures.buildconfig=true

View File

@@ -1,6 +1,7 @@
plugins { plugins {
id 'org.autojs.build.utils' id 'org.autojs.build.utils'
id 'org.autojs.build.properties' id 'org.autojs.build.properties'
id 'org.autojs.build.jvm-convention'
id 'com.android.library' id 'com.android.library'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
} }
@@ -68,15 +69,6 @@ android {
} }
} }
compileOptions {
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
}
kotlinOptions {
jvmTarget = project.ext.javaVersion
}
externalNativeBuild { externalNativeBuild {
cmake { cmake {
path "CMakeLists.txt" path "CMakeLists.txt"

View File

@@ -9,6 +9,7 @@
plugins { plugins {
id 'org.autojs.build.utils' id 'org.autojs.build.utils'
id 'org.autojs.build.properties' id 'org.autojs.build.properties'
id 'org.autojs.build.jvm-convention'
id 'com.android.library' id 'com.android.library'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
} }
@@ -125,15 +126,6 @@ android {
} }
} }
compileOptions {
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
}
kotlinOptions {
jvmTarget = project.ext.javaVersion
}
externalNativeBuild { externalNativeBuild {
cmake { cmake {
path "src/main/cpp/CMakeLists.txt" path "src/main/cpp/CMakeLists.txt"

View File

@@ -7,6 +7,7 @@
plugins { plugins {
id 'org.autojs.build.utils' id 'org.autojs.build.utils'
id 'org.autojs.build.properties' id 'org.autojs.build.properties'
id 'org.autojs.build.jvm-convention'
id 'com.android.library' id 'com.android.library'
id 'org.jetbrains.kotlin.android' id 'org.jetbrains.kotlin.android'
id 'kotlin-parcelize' id 'kotlin-parcelize'
@@ -90,15 +91,6 @@ android {
} }
} }
compileOptions {
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
}
kotlinOptions {
jvmTarget = project.ext.javaVersion
}
externalNativeBuild { externalNativeBuild {
cmake { cmake {
path "src/main/cpp/CMakeLists.txt" path "src/main/cpp/CMakeLists.txt"

View File

@@ -1,5 +1,6 @@
plugins { plugins {
id 'org.autojs.build.versions' id 'org.autojs.build.versions'
id 'org.autojs.build.jvm-convention'
id 'com.android.library' id 'com.android.library'
id 'kotlin-android' id 'kotlin-android'
} }
@@ -17,15 +18,6 @@ android {
consumerProguardFiles "consumer-rules.pro" consumerProguardFiles "consumer-rules.pro"
multiDexEnabled true multiDexEnabled true
} }
compileOptions {
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
}
kotlinOptions {
jvmTarget = project.ext.javaVersion
}
} }
dependencies { dependencies {

View File

@@ -1,5 +1,6 @@
plugins { plugins {
id 'org.autojs.build.versions' id 'org.autojs.build.versions'
id 'org.autojs.build.jvm-convention'
id 'com.android.library' id 'com.android.library'
id 'kotlin-android' id 'kotlin-android'
} }
@@ -18,15 +19,6 @@ android {
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
} }
compileOptions {
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
}
kotlinOptions {
jvmTarget = project.ext.javaVersion
}
sourceSets { sourceSets {
main { main {
java.srcDirs = ['src/main/java'] java.srcDirs = ['src/main/java']

View File

@@ -1,5 +1,6 @@
plugins { plugins {
id 'org.autojs.build.versions' id 'org.autojs.build.versions'
id 'org.autojs.build.jvm-convention'
id 'com.android.library' id 'com.android.library'
id 'kotlin-android' id 'kotlin-android'
} }
@@ -18,15 +19,6 @@ android {
versionName '1.1.0' versionName '1.1.0'
} }
compileOptions {
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
}
kotlinOptions {
jvmTarget = project.ext.javaVersion
}
lintOptions { lintOptions {
abortOnError false abortOnError false
} }

View File

@@ -1,5 +1,6 @@
plugins { plugins {
id 'org.autojs.build.versions' id 'org.autojs.build.versions'
id 'org.autojs.build.jvm-convention'
id 'com.android.library' id 'com.android.library'
id 'kotlin-android' id 'kotlin-android'
} }
@@ -19,15 +20,6 @@ android {
testInstrumentationRunner = 'androidx.test.runner.AndroidJUnitRunner' testInstrumentationRunner = 'androidx.test.runner.AndroidJUnitRunner'
} }
compileOptions {
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
}
kotlinOptions {
jvmTarget = project.ext.javaVersion
}
sourceSets { sourceSets {
main { main {
// 主 Java/Kotlin 目录 // 主 Java/Kotlin 目录

View File

@@ -1,5 +1,6 @@
plugins { plugins {
id 'org.autojs.build.versions' id 'org.autojs.build.versions'
id 'org.autojs.build.jvm-convention'
id 'com.android.library' id 'com.android.library'
id 'kotlin-android' id 'kotlin-android'
} }
@@ -25,15 +26,6 @@ android {
} }
} }
compileOptions {
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
}
kotlinOptions {
jvmTarget = project.ext.javaVersion
}
lintOptions { lintOptions {
abortOnError false abortOnError false
} }

View File

@@ -1,5 +1,6 @@
plugins { plugins {
id 'org.autojs.build.versions' id 'org.autojs.build.versions'
id 'org.autojs.build.jvm-convention'
id 'com.android.library' id 'com.android.library'
id 'kotlin-android' id 'kotlin-android'
} }
@@ -19,15 +20,6 @@ android {
consumerProguardFiles 'progress-proguard.txt' consumerProguardFiles 'progress-proguard.txt'
} }
compileOptions {
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
}
kotlinOptions {
jvmTarget = project.ext.javaVersion
}
lintOptions { lintOptions {
abortOnError false abortOnError false
} }

View File

@@ -509,6 +509,7 @@ pluginManagement {
Classpath(id = "org.apache.commons:commons-compress", version = "toml:commons-compress"), Classpath(id = "org.apache.commons:commons-compress", version = "toml:commons-compress"),
Classpath(id = "org.tukaani:xz", version = "toml:xz"), Classpath(id = "org.tukaani:xz", version = "toml:xz"),
Plugin(id = "com.google.devtools.ksp", version = overriddenKspVersion ?: "auto:ksp"), Plugin(id = "com.google.devtools.ksp", version = overriddenKspVersion ?: "auto:ksp"),
Plugin(id = "org.gradle.toolchains.foojay-resolver-convention", version = "toml:foojay-resolver-convention"),
) )
// @AnchorBegin KSP_VERSION_MAP // @AnchorBegin KSP_VERSION_MAP
@@ -987,3 +988,10 @@ pluginManagement {
} }
} }
plugins {
// @Hint by SuperMonster003 on Sep 14, 2025.
// ! Enable JDK auto-resolution/download capability for build modules.
// ! zh-CN: 让构建模块具备 JDK 自动解析/下载能力.
id("org.gradle.toolchains.foojay-resolver-convention")
}