diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index d585f090..da142967 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -37,7 +37,8 @@ "应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_", "Gradle 构建脚本提升 7z 格式文件的解压效率", "使用版本目录 (Version Catalogs) 集中管理 Gradle 依赖和插件版本", - "模块化 Gradle 脚本, 将共享构建逻辑迁移至 buildSrc 并抽象为约定插件" + "模块化 Gradle 脚本, 将共享构建逻辑迁移至 buildSrc 并抽象为约定插件", + "使用 Toolchain 替代 sourceCompatibility/targetCompatibility 以降低构建环境差异" ], "dependency": [ "本地化 Root Shell 版本 1.6", diff --git a/.utils/run-scrapers.mjs b/.utils/run-scrapers.mjs index 53b0fad2..6fc957a5 100644 --- a/.utils/run-scrapers.mjs +++ b/.utils/run-scrapers.mjs @@ -34,7 +34,8 @@ const childProcessOutput = []; * @returns {string} */ 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}`; } /** diff --git a/.utils/scrape-and-inject-android-studio-codename_maps.mjs b/.utils/scrape-and-inject-android-studio-codename_maps.mjs index 411cd057..eb5bf131 100644 --- a/.utils/scrape-and-inject-android-studio-codename_maps.mjs +++ b/.utils/scrape-and-inject-android-studio-codename_maps.mjs @@ -2,39 +2,70 @@ /** @typedef {import('puppeteer').Page} Page */ /** @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 */ /** * @template {Node} T * @typedef {import('puppeteer').ElementHandle} 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 { compareVersionStrings } from './utils/versioning.mjs'; -import { toUpdatedStamp, toYYYYMMDD } from './utils/date.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 { 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).
+ * Default is first letter, e.g. 'Meerkat' and 'Bumblebee' gives { Meerkat: 'M', Bumblebee: 'B' }.
+ * When first letters conflict, conflicts are auto-resolved, + * e.g. 'Camel' and 'Catfish' gives { Camel: 'CAM', Catfish: 'CAT' }.
+ * For extreme cases where conflicts cannot be auto-resolved, + * e.g. 'Cat' and 'Catfish', manual prefix mapping is needed, like { Cat: 'CT', Catfish: 'CTF' }.
+ * zh-CN:
+ * 手动覆盖代号映射 (可用于解决代号前缀冲突).
+ * 默认为首字母, 如 'Meerkat' 与 'Bumblebee', 得到 { Meerkat: 'M', Bumblebee: 'B' }.
+ * 首字母重复时自动消解冲突, 如 'Camel' 与 'Catfish', 得到 { Camel: 'CAM', Catfish: 'CAT' }.
+ * 极端情况无法自动消解冲突, 如 'Cat' 与 'Catfish', 此时需要手动指定前缀, 如 { Cat: 'CT', Catfish: 'CTF' }. + * @example Object + * { + * 'Camel': 'CM', + * 'Cat': 'CT', + * 'Catfish': 'CTF', + * ... ... + * } + * @type {Object} + */ +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.
+ * zh-CN: 用 "最新稳定版" 的校验和/文件名, 在归档中定位条目, 补全并更新 common.json. + * + * @param {ArchiveItem[]} archives + * @returns {Promise} + */ +async function updateLatestArchiveInfo(archives) { + const latestRows = await getLatestStableArchives(); // [ { kind, filename, sha256, url, size, ... } ] const latestExe = latestRows.find(x => x.kind === 'exe'); const latestZip = latestRows.find(x => x.kind === 'zip'); const latestTar = latestRows.find(x => x.kind === 'tar'); 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.
+ * zh-CN: 在归档中查找: 优先用 sha256 命中, 其次用文件名. * * @param {ArchiveItem[]} rows * @param {Row} target - * @return {ArchiveItem | null} + * @returns {ArchiveItem | null} */ const matchArchive = (rows, target) => { for (const arc of rows) { @@ -46,29 +77,29 @@ async function main() { }; const matchedArc = matchArchive(archives, latestExe) || matchArchive(archives, latestZip); 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 - * @return {{ filename: string, url: string, sizeGiB?: string | null } | null} + * @returns {{ filename: string, url: string, sizeGiB?: string | null } | null} */ - const pickWinItem = suffix => { - // suffix: "-windows.exe" | "-windows.zip" + const pickWinItem = (suffix) => { const link = matchedArc.links.find(l => l.text.endsWith(suffix)); if (!link) return null; - return { - filename: link.text, - url: link.href, - }; + return { filename: link.text, url: link.href }; }; const exeItem = pickWinItem('-windows.exe'); const zipItem = pickWinItem('-windows.zip'); 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([ exeItem ? getRemoteFileSizeBytes(exeItem.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 (tarItem) tarItem.sizeGiB = bytes2GiB(tarBytes); - // 准备写回 common.json 所需字段 - const latestVersionName = matchedArc.title.trim(); // 例: "Android Studio Narwhal Feature Drop | 2025.1.2" - const latestVersionDate = toYYYYMMDD(matchedArc.date) || ''; // 例: "2025/07/31" + // Prepare fields needed to write back to common.json. + // zh-CN: 准备写回 common.json 所需字段. - if (!exeItem || !zipItem) { - throw new Error('匹配到的归档条目缺少 Windows EXE 或 ZIP 下载信息'); - } + const latestVersionName = matchedArc.title.trim(); // e.g. "Android Studio Narwhal Feature Drop | 2025.1.2" + const latestVersionDate = toYYYYMMDD(matchedArc.date) || ''; // e.g. "2025/07/31" - // 读取并更新 common.json (仅更新带有 "android_studio_latest_" 片段的键与版本名日期键) - const fs = await import('node:fs/promises'); - const path = await import('node:path'); + // Read and update common.json (only update keys containing "android_studio_latest_" fragment and version name date key). + // zh-CN: 读取并更新 common.json (仅更新带有 "android_studio_latest_" 片段的键与版本名日期键). 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 updatedCommon = { @@ -110,60 +138,112 @@ async function main() { }; if (JSON.stringify(updatedCommon) !== JSON.stringify(commonObj)) { - await fs.writeFile(commonJsonPath, JSON.stringify(updatedCommon, null, 2), 'utf8'); - console.log('[common.json] 已更新 (Android Studio 数据)'); - console.log(`-- '${commonObj.android_studio_latest_recommended_version_name}'`); - console.log(`-> '${updatedCommon.android_studio_latest_recommended_version_name}'`); + await fsp.writeFile(commonJsonPath, JSON.stringify(updatedCommon, null, 2), 'utf8'); + console.log('[common.json] Updated (Android Studio information)'); + if (commonObj.android_studio_latest_recommended_version_name !== 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 { - // 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.
+ * 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 - * @return {string | null} + * Parse "codename and version number" from title, + * zh-CN: 从标题中解析 "代号与版本号", + * + * @example string + * 'Android Studio Meerkat Feature Drop | 2024.3.2 RC 1' + * + * @param {string} title + * @returns {string | null} */ - const codenameFromTitle = t => { - // 捕获 "Android Studio [Feature Drop] |" - const m = /Android Studio\s+(.+?)\s*(?:(\s+\d+\s+)?Feature Drop)?\s*\|/i.exec(t); + const codenameFromTitle = title => { + // Capture "Android Studio [Feature Drop]". + // zh-CN: 捕获 "Android Studio [Feature Drop]". + const m = /Android Studio\s+(.+?)\s*(?:(\s+\d+\s+)?Feature Drop)?(?=\s*\|)/i.exec(title); return m ? m[1].trim() : null; }; - // 用户手动覆盖映射 (可按需填写或从外部读取) - /** @type {Object} */ - const manualCodenameOverrides = { - /* e.g. 'Meerkat': 'Mkt' */ - }; - - // 生成唯一代码: 按顺序, 出现冲突则对冲突组统一递增长度; 覆盖项优先生效且不可被自动改动 /** - * @param {string[]} names 原始代号 (保留大小写与空格, 顺序与站点一致) - * @param {Object} overrides 手动覆盖映射 - * @returns {Map} name -> code + * Generate unique codes: sequentially, uniformly increase length for conflict groups; + * override mappings take precedence.
+ * zh-CN:
+ * 生成唯一代码: 按顺序, 出现冲突则对冲突组统一递增长度; 覆盖映射优先考虑. + * + * @example Map + * 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).
+ * zh-CN: 原始代号 (保留大小写与空格, 如 "Arctic Fox", "Bumblebee", "Narwhal" 等). + * @param {Object} overrides + * Manual override mappings.
+ * zh-CN: 手动覆盖映射. + * @returns {Map} */ - function 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], - })); + const buildUniquePrefixes = (names, overrides) => { - // 先赋初值 - for (const e of entries) { - if (!e.code) e.code = e.base.slice(0, e.len); - } + /** + * @param {string} s + * @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 ])); - // 循环上限保护, 防止极端情况下死循环 - for (let step = 0; step < 1024; step++) { - // 统计冲突组: code -> indices - /** @type {Map} */ + // Loop limit protection to prevent infinite loops in extreme cases. + // zh-CN: 循环上限保护, 防止极端情况下死循环. + for (let step = 0; step < 1 << 10; step++) { + /** + * Count conflict groups. (zh-CN: 统计冲突组.) + * @example Map + * Map(14) { + * 'N' => [ 0 ], + * 'M' => [ 1 ], + * ... ... + * 'B' => [ 12 ], + * 'A' => [ 13 ] + * } + * @type {Map} + */ const bucket = new Map(); entries.forEach((e, idx) => { const key = e.code; @@ -171,30 +251,26 @@ async function main() { bucket.get(key).push(idx); }); - // 找到有冲突的组 (size >= 2) - const conflicts = Array.from(bucket.entries()).filter(([ , indices ]) => indices.length >= 2); - if (conflicts.length === 0) break; // 已无冲突 + const conflicts = Array.from(bucket.entries()).filter(([ _, indices ]) => indices.length >= 2); + if (conflicts.length === 0) { + // No conflicts, or all conflicts have been resolved. + // zh-CN: 没有冲突, 或冲突已全部解决. + break; + } - // 逐组处理 for (const [ code, indices ] of conflicts) { - // 若组内存在 >=2 个锁定项且 code 相同 -> 直接报错 const lockedIndices = indices.filter(i => entries[i].locked); if (lockedIndices.length >= 2) { - const letter = code[0]?.toUpperCase() || '?'; 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) { const e = entries[i]; - if (e.locked) continue; // 覆盖项不改 + if (e.locked) continue; const maxLen = maxLenByName.get(e.name); if (e.len >= maxLen) { - // 已经用到全长仍冲突 -> 无法自动消解 - const letter = e.base[0]?.toUpperCase() || '?'; - const groupNames = indices.map(ii => entries[ii].name); - throw new Error(`[CodenameMap] 无法自动消解冲突 ("${e.name}" 与同组名称完全相同或为彼此前缀到尽头). 请为该首字母组手动指定覆盖映射.\n- 组首字母: ${letter}\n- 组成员: ${groupNames.join(', ')}`); + const groupNames = indices.map(i => entries[i].name); + throw new Error(`[CodenameMap] Unable to auto-resolve conflicts: [ ${groupNames.join(', ')} ]`); } e.len += 1; 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 ])); - } + }; - // 提取按页面顺序的代号列表 (去重, 保留第一次出现顺序) - const codenamesOrdered = []; - 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); - } - } + const codenames = [ ...new Set(archives.map(o => codenameFromTitle(o.title)).filter(Boolean)) ]; + const nameToCode = buildUniquePrefixes(codenames, manualCodenameOverrides); - // 构建 name->code 映射 (按规则自动消解冲突; 支持手动覆盖) - const nameToCode = buildUniquePrefixes(codenamesOrdered, manualCodenameOverrides); - - // 收集每版本 (yyyy.m.patch) 对应的代号集合, 以及每个代号的首发日期 - /** @type {Map>} */ + /** + * @example Map> + * Map(22) { + * ... ... + * '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>} + */ const versionToLetters = new Map(); - /** @type {Map} */ + + /** + * @example Map + * 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} + */ const letterBorn = new Map(); for (const arc of archives) { - if (!arc.version) continue; // 版本号 (yyyy.m.patch) + if (!arc.version) continue; const cname = codenameFromTitle(arc.title); if (!cname) continue; const code = nameToCode.get(cname); if (!code) continue; - // 映射 version -> codes if (!versionToLetters.has(arc.version)) versionToLetters.set(arc.version, new Set()); versionToLetters.get(arc.version).add(code); - // 记录代号首次出现日期 const d = new Date(arc.date); const existed = letterBorn.get(code); if (!existed || d < existed.born) { @@ -246,40 +332,100 @@ async function main() { } } - // 生成 codenameVersionMap 文本 (按版本倒序排列, 值用 "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 [ by, bm, bp ] = b.split('.').map(Number); 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('|') ]); - /* e.g. { "2023.3": {1: "J", 2: "J|K"} }. */ - /** @type {Object} */ + /** + * @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} + */ const rawVersionLettersMap = {}; for (let i = 0; i < versionLettersList.length; i++) { const [ v, letters ] = versionLettersList[i]; const matched = v.match(/(^\d+\.\d+)(?:\.(\d+))?/); if (!matched) continue; - const [ , major, patch ] = matched; - if (major in rawVersionLettersMap) { - rawVersionLettersMap[major][patch] = letters; + const [ , minorSeries, patch ] = matched; + if (minorSeries in rawVersionLettersMap) { + rawVersionLettersMap[minorSeries][patch] = letters; } 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).
+ * zh-CN:
+ * 当次版本系列相同的版本 (如 `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 = {}; - Object.entries(rawVersionLettersMap).forEach(([ major, patchToLetters ]) => { + Object.entries(rawVersionLettersMap).forEach(([ minorSeries, patchToLetters ]) => { const letterValues = Object.values(patchToLetters); if (new Set(letterValues).size === 1) { - combinedVersionLettersMap[major] = letterValues[0]; + /* Combine. (zh-CN: 合并.) */ + combinedVersionLettersMap[minorSeries] = letterValues[0]; } else { + /* Keep expanded. (zh-CN: 保持展开.) */ 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])) .map(([ v, letters ]) => `"${v}" to "${letters}",`); - // 生成 codenameMap 文本 (按 born 日期倒序; 注释: Born on Mon d, yyyy.) - // 这里 key 为自动生成的 code (可能为一到多字符), value 为完整代号 const sortedLetters = Array.from(letterBorn.entries()) .sort((a, b) => b[1].born.getTime() - a[1].born.getTime()); const codenameMapLines = sortedLetters.map(([ code, { name, born } ]) => { const bornStr = toUpdatedStamp(born); + // e.g. `"M" to "Meerkat", /* Born on Nov 12, 2024. */`. 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', [ { type: 'map', anchorTag: 'ANDROID_STUDIO_CODENAME_VERSION_MAP', mapName: 'codenameVersionMap', lines: versionMapLines, - updatedLabel: 'Android Studio 代号版本映射', + updatedLabel: 'Android Studio codename version map', }, { type: 'map', anchorTag: 'ANDROID_STUDIO_CODENAME_MAP', mapName: 'codenameMap', lines: codenameMapLines, - updatedLabel: 'Android Studio 代号名称映射', + updatedLabel: 'Android Studio codename map', } ]); -} - -main().catch(err => { +})().catch(err => { console.error(err); process.exitCode = 1; -}); \ No newline at end of file +}); diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 9bf4fbe8..b6771f5e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -10,6 +10,7 @@ plugins { id("org.autojs.build.utils") id("org.autojs.build.versions") id("org.autojs.build.signs") + id("org.autojs.build.jvm-convention") id("com.android.application") id("com.google.devtools.ksp") id("org.jetbrains.kotlin.android") /* kotlin("android") */ @@ -567,8 +568,6 @@ android { compileOptions { isCoreLibraryDesugaringEnabled = true - sourceCompatibility = versions.javaVersion - targetCompatibility = versions.javaVersion } // @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 { abortOnError = false } @@ -745,12 +738,6 @@ tasks { // options.compilerArgs.addAll(listOf("-Xlint:deprecation", "-Xlint:unchecked")) } - withType().configureEach { - // @Archived by SuperMonster003 on Aug 14, 2024. - // # kotlinOptions { jvmTarget = versions.javaVersion.toString() } - compilerOptions { jvmTarget.set(JvmTarget.valueOf("JVM_${versions.javaVersion}")) } - } - register("appendDigestToReleasedFiles") { listOf(flavorNameApp, flavorNameInrt).forEach { flavorName -> val src = "$flavorName/$buildTypeRelease" diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index fcdef6b4..0a108f97 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -52,6 +52,14 @@ gradlePlugin { t.description = "Provides properties helpers." } }) + create("jvmConvention", object : Action { + 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." + } + }) } } diff --git a/buildSrc/settings.gradle.kts b/buildSrc/settings.gradle.kts index 9fed38fa..ae380fa3 100644 --- a/buildSrc/settings.gradle.kts +++ b/buildSrc/settings.gradle.kts @@ -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") +} diff --git a/buildSrc/src/main/kotlin/org/autojs/build/JvmConventionPlugin.kt b/buildSrc/src/main/kotlin/org/autojs/build/JvmConventionPlugin.kt new file mode 100644 index 00000000..e73ebc3e --- /dev/null +++ b/buildSrc/src/main/kotlin/org/autojs/build/JvmConventionPlugin.kt @@ -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` 中应用此插件:
+ * + * ```kts + * plugins { + * id("org.autojs.build.jvm-convention") + * } + * ``` + */ +class JvmConventionPlugin : Plugin { + override fun apply(project: Project) { + Utils.configureJvmForAndroidModule(project) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 3724ba6c..e2dc57d2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,6 +11,10 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true #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 # https://docs.gradle.org/current/userguide/gradle_daemon.html org.gradle.daemon=true @@ -35,7 +39,13 @@ android.enableJetifier=true # thereby reducing the size of the R class for that library android.nonTransitiveRClass=true android.nonFinalResIds=true -# -- archived fields -- -# Added since Gradle Build Tool version 8.0.0-alpha09 -# Removed since Gradle Build Tool version 8.2.0-alpha15 +# Configure Java installation settings +# auto-detect: Automatically detect Java installations on the system +# 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 diff --git a/libs/imagequant/build.gradle b/libs/imagequant/build.gradle index a854dc98..b02afc35 100644 --- a/libs/imagequant/build.gradle +++ b/libs/imagequant/build.gradle @@ -1,6 +1,7 @@ plugins { id 'org.autojs.build.utils' id 'org.autojs.build.properties' + id 'org.autojs.build.jvm-convention' id 'com.android.library' 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 { cmake { path "CMakeLists.txt" diff --git a/libs/paddleocr/build.gradle b/libs/paddleocr/build.gradle index 41f297bb..3e8d6824 100644 --- a/libs/paddleocr/build.gradle +++ b/libs/paddleocr/build.gradle @@ -9,6 +9,7 @@ plugins { id 'org.autojs.build.utils' id 'org.autojs.build.properties' + id 'org.autojs.build.jvm-convention' id 'com.android.library' 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 { cmake { path "src/main/cpp/CMakeLists.txt" diff --git a/libs/rapidocr/build.gradle b/libs/rapidocr/build.gradle index eb0aef46..e7cace83 100644 --- a/libs/rapidocr/build.gradle +++ b/libs/rapidocr/build.gradle @@ -7,6 +7,7 @@ plugins { id 'org.autojs.build.utils' id 'org.autojs.build.properties' + id 'org.autojs.build.jvm-convention' id 'com.android.library' id 'org.jetbrains.kotlin.android' 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 { cmake { path "src/main/cpp/CMakeLists.txt" diff --git a/modules/apk-parser/build.gradle b/modules/apk-parser/build.gradle index 7233ba6f..f27f40ae 100644 --- a/modules/apk-parser/build.gradle +++ b/modules/apk-parser/build.gradle @@ -1,5 +1,6 @@ plugins { id 'org.autojs.build.versions' + id 'org.autojs.build.jvm-convention' id 'com.android.library' id 'kotlin-android' } @@ -17,15 +18,6 @@ android { consumerProguardFiles "consumer-rules.pro" multiDexEnabled true } - - compileOptions { - sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion) - targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion) - } - - kotlinOptions { - jvmTarget = project.ext.javaVersion - } } dependencies { diff --git a/modules/apk-signer/build.gradle b/modules/apk-signer/build.gradle index f1966431..7c2c9c65 100644 --- a/modules/apk-signer/build.gradle +++ b/modules/apk-signer/build.gradle @@ -1,5 +1,6 @@ plugins { id 'org.autojs.build.versions' + id 'org.autojs.build.jvm-convention' id 'com.android.library' id 'kotlin-android' } @@ -18,15 +19,6 @@ android { testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' } - compileOptions { - sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion) - targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion) - } - - kotlinOptions { - jvmTarget = project.ext.javaVersion - } - sourceSets { main { java.srcDirs = ['src/main/java'] diff --git a/modules/color-picker/build.gradle b/modules/color-picker/build.gradle index 293c07a9..bf080935 100644 --- a/modules/color-picker/build.gradle +++ b/modules/color-picker/build.gradle @@ -1,5 +1,6 @@ plugins { id 'org.autojs.build.versions' + id 'org.autojs.build.jvm-convention' id 'com.android.library' id 'kotlin-android' } @@ -18,15 +19,6 @@ android { versionName '1.1.0' } - compileOptions { - sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion) - targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion) - } - - kotlinOptions { - jvmTarget = project.ext.javaVersion - } - lintOptions { abortOnError false } diff --git a/modules/jieba-analysis/build.gradle b/modules/jieba-analysis/build.gradle index 7f47d244..59695e2c 100644 --- a/modules/jieba-analysis/build.gradle +++ b/modules/jieba-analysis/build.gradle @@ -1,5 +1,6 @@ plugins { id 'org.autojs.build.versions' + id 'org.autojs.build.jvm-convention' id 'com.android.library' id 'kotlin-android' } @@ -19,15 +20,6 @@ android { testInstrumentationRunner = 'androidx.test.runner.AndroidJUnitRunner' } - compileOptions { - sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion) - targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion) - } - - kotlinOptions { - jvmTarget = project.ext.javaVersion - } - sourceSets { main { // 主 Java/Kotlin 目录 diff --git a/modules/material-date-time-picker/build.gradle b/modules/material-date-time-picker/build.gradle index 6a6e91e9..dbdcf9ed 100644 --- a/modules/material-date-time-picker/build.gradle +++ b/modules/material-date-time-picker/build.gradle @@ -1,5 +1,6 @@ plugins { id 'org.autojs.build.versions' + id 'org.autojs.build.jvm-convention' id 'com.android.library' 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 { abortOnError false } diff --git a/modules/material-dialogs/build.gradle b/modules/material-dialogs/build.gradle index f9dc4785..4270f8d6 100644 --- a/modules/material-dialogs/build.gradle +++ b/modules/material-dialogs/build.gradle @@ -1,5 +1,6 @@ plugins { id 'org.autojs.build.versions' + id 'org.autojs.build.jvm-convention' id 'com.android.library' id 'kotlin-android' } @@ -19,15 +20,6 @@ android { consumerProguardFiles 'progress-proguard.txt' } - compileOptions { - sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion) - targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion) - } - - kotlinOptions { - jvmTarget = project.ext.javaVersion - } - lintOptions { abortOnError false } diff --git a/settings.gradle.kts b/settings.gradle.kts index ee97e16f..77751b9f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -509,6 +509,7 @@ pluginManagement { Classpath(id = "org.apache.commons:commons-compress", version = "toml:commons-compress"), Classpath(id = "org.tukaani:xz", version = "toml:xz"), 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 @@ -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") +}