From 0b01bba6184ea1207ceb0f0ad5fb0021ec0932fe Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Thu, 2 Oct 2025 13:11:54 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha6=20-=20Scrapers=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E7=9A=84=E6=9B=B4=E6=96=B0=E6=9C=BA=E5=88=B6=E7=94=B1?= =?UTF-8?q?=20"=E9=94=9A=E7=82=B9=E6=9B=B4=E6=96=B0"=20=E6=9B=BF=E6=8D=A2?= =?UTF-8?q?=E4=B8=BA=20"=E7=BB=93=E6=9E=84=E5=8C=96=E6=9B=B4=E6=96=B0"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .changelog/lang_zh-Hans.json | 7 +- .utils/declarations/index.d.ts | 88 ++- ...etch-and-parse-android-studio-archives.mjs | 225 ++----- ...e-android-studio-latest-stable-version.mjs | 122 ---- .utils/run-scrapers.mjs | 16 +- ...d-inject-agp-gradle-compatibility-map.mjs} | 19 +- ...> scrape-and-inject-agp-releases-list.mjs} | 18 +- ...-inject-android-studio-agp-version-map.mjs | 23 +- ...nd-inject-android-studio-codename-maps.mjs | 563 ++++++++++++++++++ ...nd-inject-android-studio-codename_maps.mjs | 467 --------------- ...nject-gradle-kotlin-compatibility-list.mjs | 58 -- ...inject-gradle-kotlin-compatibility-map.mjs | 48 ++ ...-inject-java-gradle-compatibility-map.mjs} | 32 +- ...=> scrape-and-inject-ksp-releases-map.mjs} | 44 +- ...crape-and-inject-latest-gradle-wrapper.mjs | 6 +- ...ate-readme-template-contributors-table.mjs | 6 +- .utils/utils/anchors.mjs | 266 --------- .utils/utils/print.mjs | 26 +- .utils/utils/properties.mjs | 114 ++-- .utils/utils/sorting.mjs | 75 +++ .utils/utils/update-helper.mjs | 125 ++++ .../autojs/runtime/api/augment/app/App.kt | 2 - gradle/data/agp-gradle-compat.properties | 13 + gradle/data/agp-releases.list | 14 + .../data/android-studio-agp-compat.properties | 11 + .../android-studio-build-version.properties | 154 +++++ ...android-studio-codename-version.properties | 20 + .../data/android-studio-codename.properties | 31 + gradle/data/gradle-kotlin-compat.properties | 11 + gradle/data/java-gradle-compat.properties | 11 + gradle/data/ksp-releases.properties | 135 +++++ settings.gradle.kts | 402 ++++--------- version.properties | 12 +- 33 files changed, 1599 insertions(+), 1565 deletions(-) delete mode 100644 .utils/fetch-and-parse-android-studio-latest-stable-version.mjs rename .utils/{scrape-and-inject-agp-gradle-compatibility-list.mjs => scrape-and-inject-agp-gradle-compatibility-map.mjs} (67%) rename .utils/{scrape-and-inject-agp-releases.mjs => scrape-and-inject-agp-releases-list.mjs} (53%) create mode 100644 .utils/scrape-and-inject-android-studio-codename-maps.mjs delete mode 100644 .utils/scrape-and-inject-android-studio-codename_maps.mjs delete mode 100644 .utils/scrape-and-inject-gradle-kotlin-compatibility-list.mjs create mode 100644 .utils/scrape-and-inject-gradle-kotlin-compatibility-map.mjs rename .utils/{scrape-and-inject-java-gradle-compatibility-list.mjs => scrape-and-inject-java-gradle-compatibility-map.mjs} (51%) rename .utils/{scrape-and-inject-ksp-releases.mjs => scrape-and-inject-ksp-releases-map.mjs} (79%) delete mode 100644 .utils/utils/anchors.mjs create mode 100644 .utils/utils/sorting.mjs create mode 100644 .utils/utils/update-helper.mjs create mode 100644 gradle/data/agp-gradle-compat.properties create mode 100644 gradle/data/agp-releases.list create mode 100644 gradle/data/android-studio-agp-compat.properties create mode 100644 gradle/data/android-studio-build-version.properties create mode 100644 gradle/data/android-studio-codename-version.properties create mode 100644 gradle/data/android-studio-codename.properties create mode 100644 gradle/data/gradle-kotlin-compat.properties create mode 100644 gradle/data/java-gradle-compat.properties create mode 100644 gradle/data/ksp-releases.properties diff --git a/.changelog/lang_zh-Hans.json b/.changelog/lang_zh-Hans.json index d51d4276..d1c2444a 100644 --- a/.changelog/lang_zh-Hans.json +++ b/.changelog/lang_zh-Hans.json @@ -1,14 +1,14 @@ { "$data": { "v6.7.0": { - "released_date": "2025/09/28", + "released_date": "2025/10/02", "feature": [ "zip 模块, 用于文件压缩与解压缩操作 (Ref to [Auto.js Pro](https://g.pro.autojs.org/)) (参阅 项目文档 > [Zip](https://docs.autojs6.com/#/zip))", "mediainfo 模块, 用于查看媒体文件的详细信息 (参阅 项目文档 > [媒体信息](https://docs.autojs6.com/#/mediainfo))", "UiObject#isShifted 方法, 用于检测控件位置变化", "structuredClone 全局方法, 用于深拷贝 JavaScript 对象 (参阅 [MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Window/structuredClone))", "设置页面支持应用启动器图标设置选项 _[`issue #405`](http://issues.autojs6.com/405)_", - "JS 脚本工具 (run-scrapers.mjs) 用于自动更新 Gradle 构建脚本锚点数据/README 通用数据/README 模板数据" + "Scrapers 工具 (run-scrapers.mjs) 用于自动更新 Gradle 构建脚本结构化数据/README 通用数据/README 模板数据等" ], "fix": [ "isJavaClass/isJavaPackage 等全局方法无效的问题", @@ -39,8 +39,9 @@ "应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_", "使用 LiveData 及 SharedFlow 替代已弃用的 LocalBroadcastManager", "Gradle 构建脚本提升 7z 格式文件的解压效率", + "Gradle 构建脚本支持获取详细的 Android Studio IDE 版本 (如 \"2025.1.4.7\")", "使用版本目录 (Version Catalogs) 集中管理 Gradle 依赖和插件版本", - "模块化 Gradle 脚本, 将共享构建逻辑迁移至 buildSrc 并抽象为约定插件", + "模块化 Gradle 构建脚本, 将共享构建逻辑迁移至 buildSrc 并抽象为约定插件", "使用 Toolchain 替代 sourceCompatibility/targetCompatibility 以降低构建环境差异" ], "dependency": [ diff --git a/.utils/declarations/index.d.ts b/.utils/declarations/index.d.ts index bbbdcd3c..0e7b2829 100644 --- a/.utils/declarations/index.d.ts +++ b/.utils/declarations/index.d.ts @@ -7,32 +7,43 @@ type ReleasesData = import('@octokit/types').Endpoints['GET /repos/{owner}/{repo type PullsData = import('@octokit/types').Endpoints['GET /repos/{owner}/{repo}/pulls']['response']['data']; type PullCommitsData = import('@octokit/types').Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}/commits']['response']['data']; -type AndroidStudioStableArchiveItemKind = 'exe' | 'zip' | 'tar' | 'other'; - type FindTargetRowsFilter = string | RegExp | ((s: string) => boolean); type TableDataStructureItemName = string; type TableDataStructureItem = RegExp | ((s: string) => boolean | string); type TableDataStructureItemForPageEvaluate = string | RegExp | ((s: string) => boolean | string); -interface AndroidStudioArchiveItem { - title: string; - date: string; - version: string | null; - links: Array<{ - text: string; - href: string; - }>; - checksums: { [filename: string]: string }; +interface AndroidStudioRelease { + content: { + item: AndroidStudioReleaseItem[]; + }; + version: number; } -interface AndroidStudioStableArchiveItem { - platform: string; - filename: string; +interface AndroidStudioReleaseItem { + /** @example 'September 29, 2025' */ + date: string; + /** @example '251.27812.49' */ + platformBuild: string; + download: AndroidStudioReleaseDownloadItem[]; + /** @example 'AI-251.27812.49.2514.14171003' */ + build: string; + /** @example '2025.1.5' */ + platformVersion: string; + /** @example 'Android Studio Narwhal 4 Feature Drop | 2025.1.4 RC 2' */ + name: string; + channel: 'Preview' | 'Canary' | 'Beta' | 'RC' | 'Release' | 'Patch'; + /** @example '2025.1.4.7' */ + version: string; +} + +interface AndroidStudioReleaseDownloadItem { + /** @example '1.4 GB' */ size: string; - sha256: string; - url: string | null; - kind: AndroidStudioStableArchiveItemKind; + /** @example 'https://redirector.gvt1.com/edgedl/android/studio/ide-zips/2025.1.4.7/android-studio-2025.1.4.7-windows.zip' */ + link: string; + /** @example '91b48f1561cda0387e7499fa7e425908aa5f1235ce36aec1383fa7091f5c242a' */ + checksum: string; } interface FindTargetRowsOptionsBase { @@ -114,13 +125,38 @@ interface ScriptItem { abs: string; } -interface AnchoredBlockUpdateOption { - type: 'map' | 'list' | 'custom'; - anchorTag: string; - mapName?: string; - listName?: string; - lines: string[]; - linesIndent?: number; - updatedLabel?: string; - replacer?: (srcInBlock: string, options: { toUpdatedStamp?: (date?: Date) => string }) => { newBlock: string, changed: boolean }; +interface GradleMapUpdateOptions extends GradleDataUpdateOptions, GradleMapRwOptions { + /* Empty body. */ +} + +interface GradleListUpdateOptions extends GradleDataUpdateOptions, GradleListRwOptions { + /* Empty body. */ +} + +interface GradleLinesUpdateOptions extends GradleDataUpdateOptions { + /* Empty body. */ +} + +interface GradleDataUpdateOptions extends GradleDataRwOptions { + label?: string +} + +interface GradleMapRwOptions extends GradleDataRwOptions, MapSortable { + /* Empty body. */ +} + +interface GradleListRwOptions extends GradleDataRwOptions, ListSortable { + /* Empty body. */ +} + +interface GradleDataRwOptions { + encoding?: BufferEncoding; +} + +interface MapSortable { + sort?: `${'key' | 'value'}.${'ascending' | 'descending'}` | `${'key' | 'value'}.${'ascending' | 'descending'}.as.${'string' | 'version' | 'number'}`; +} + +interface ListSortable { + sort?: `${'ascending' | 'descending'}` | `${'ascending' | 'descending'}.as.${'string' | 'version' | 'number'}`; } diff --git a/.utils/fetch-and-parse-android-studio-archives.mjs b/.utils/fetch-and-parse-android-studio-archives.mjs index 194bcaef..b10d5672 100644 --- a/.utils/fetch-and-parse-android-studio-archives.mjs +++ b/.utils/fetch-and-parse-android-studio-archives.mjs @@ -1,204 +1,79 @@ // fetch-and-parse-android-studio-archives.mjs -import puppeteer from 'puppeteer'; -import { compareVersionStrings, isVersionStable } from './utils/versioning.mjs'; +import { bytes2GiB } from './utils/format.mjs'; +import { compareVersionStrings } from './utils/versioning.mjs'; import { fileURLToPath } from 'node:url'; +import { getRemoteFileSizeBytes } from './utils/fetch.mjs'; import { readPropertiesSync } from './utils/properties.mjs'; -import { sleep } from './utils/async.mjs'; -const URL = 'https://developer.android.com/studio/archive?hl=en'; -const SELECTOR_PRIMARY_BUTTON = 'button.button-primary'; -const SELECTOR_DEVSITE_EXPANDABLE = 'devsite-expandable'; +const URL = 'https://jb.gg/android-studio-releases-list.json'; /** - * Find "agree" button in frame (including main document).
- * zh-CN: 在所有 frame (含主文档) 中查找 "同意" 按钮. - * - * @param {Page} page - * @param {number} [timeoutMs=30000] - * @returns {Promise<{ handle: ElementHandle, frame: Frame }>} + * @returns {Promise} */ -async function waitAndFindAgreeButton(page, timeoutMs = 30000) { - /** - * @param {string | null} s - * @returns {boolean} - */ - const containsAgreementText = (s) => s && /\b(?:i\s*agree|agree\s*to\s*the\s*terms|^agree$)/i.test(s.trim()); - - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - - // Attempt main document. - // zh-CN: 尝试主文档. - - const btnList = await page.$$(SELECTOR_PRIMARY_BUTTON); - for (const btn of btnList) { - const txt = await page.evaluate(el => el.textContent, btn); - if (containsAgreementText(txt)) { - return { handle: btn, frame: page.mainFrame() }; - } - } - - // Check all sub-frame. - // zh-CN: 检查所有子 frame. - - const frames = page.frames(); - for (const f of frames) { - /** @type {ElementHandle} */ - const btn = await f.$(SELECTOR_PRIMARY_BUTTON); - if (!btn) continue; - const txt = await f.evaluate(el => el.textContent, btn); - if (containsAgreementText(txt)) { - return { handle: btn, frame: f }; - } - } - - // Trigger lazy-loading: scroll slightly several times. - // zh-CN: 触发懒加载, 轻微滚动几次. - - await page.evaluate(() => window.scrollBy(0, 600)); - await sleep(300); +export async function getAndroidStudioReleases() { + const res = await fetch(URL); + if (!res.ok) { + throw new Error(`Failed to fetch Android Studio releases list: ${res.status} ${res.statusText}`); } - throw new Error('Unable to find "agree" button in any document (timeout)'); + /** @type {AndroidStudioRelease} */ + const json = await res.json(); + if (!Array.isArray(((json || {}).content || {}).item)) { + throw new Error(`Invalid Android Studio releases list format: ${json}`); + } + return json.content.item; } /** - * Wait for a selector to appear in any frame and return that frame.
- * zh-CN: 在所有 frame 中等待某个选择器出现, 并返回该 frame. - * - * @param {Page} page - * @param {string} selector - * @param [timeoutMs=30000] - * @returns {Promise} + * @param {AndroidStudioReleaseItem[]} releases + * @returns {AndroidStudioReleaseItem} */ -async function waitForFrameWithSelector(page, selector, timeoutMs = 30000) { - const deadline = Date.now() + timeoutMs; - while (Date.now() < deadline) { - for (const f of page.frames()) { - if (await f.$(selector)) { - return f; - } - } - await sleep(300); - } - throw new Error(`Could not find selector "${selector}" in any frame`); +export function extractLatestStableRelease(releases) { + return releases.find(release => { + return /Release|Patch/i.test(release.channel); + }); } /** - * @returns {Promise} + * @param {AndroidStudioReleaseDownloadItem[]} downloadItems + * @param {RegExp|null} [linkFilter=null] + * @returns {Promise} */ -export async function getAndroidStudioArchives() { - const browser = await puppeteer.launch({ - headless: true, - args: [ - '--no-sandbox', - '--disable-setuid-sandbox', - ], - }); +export async function refineDownloadItemsWithRealSize(downloadItems, linkFilter = null) { + const results = linkFilter + ? downloadItems.filter(item => linkFilter.test(item.link)) + : [ ...downloadItems ]; + return await Promise.all(results.map(async (item) => { + item.size = bytes2GiB(await getRemoteFileSizeBytes(item.link)); + return item; + })); +} - const page = await browser.newPage(); - await page.setUserAgent('Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36'); - await page.goto(URL, { waitUntil: 'networkidle2', timeout: 60000 }); - - // Scroll to download area, trigger lazy loading (helpful for injecting iframe containing "agree" button). - // zh-CN: 滚动到下载区域, 触发懒加载 (有助于注入承载 "同意" 按钮的 iframe). - await page.evaluate(() => { - const anchor = document.querySelector('#downloads') - || Array.from(document.querySelectorAll('h2, h3')) - .find(h => /download|archive/i.test(h.textContent ?? '')); - if (anchor) anchor.scrollIntoView({ behavior: 'instant', block: 'start' }); - }); - await sleep(300); - - try { - const { handle, frame } = await waitAndFindAgreeButton(page, 30000); - await frame.waitForSelector(SELECTOR_PRIMARY_BUTTON, { visible: true, timeout: 15000 }).catch(_ => null); - await handle.click(); - } catch (e) { - console.log('No protocol detected or already agreed, continuing parsing...'); - } - - // Don't wait in main document after agreeing; wait for devsite-expandable in content frame instead. - // If not found initially, try slightly scrolling to trigger lazy loading and check again. - // zh-CN: - // 同意后不要在主文档等待; 改为在包含内容的 frame 里等待 devsite-expandable. - // 若首次未出现, 尝试轻微滚动以触发懒加载, 再次检查. - - /** @type {Frame} */ - let contentFrame; - try { - contentFrame = await waitForFrameWithSelector(page, SELECTOR_DEVSITE_EXPANDABLE, 20000); - } catch { - // Attempt to trigger loading by scrolling. (zh-CN: 尝试滚动触发.) - for (let i = 0; i < 8; i++) { - await page.evaluate(() => window.scrollBy(0, 800)); - await sleep(300); - } - // Check again. (zh-CN: 再次检查.) - contentFrame = await waitForFrameWithSelector(page, SELECTOR_DEVSITE_EXPANDABLE, 20000); - } - if (!contentFrame) { - throw new Error('Failed to find content frame'); - } - - /** @type {AndroidStudioArchiveItem[]} */ - const archives = await contentFrame.$$eval(SELECTOR_DEVSITE_EXPANDABLE, nodes => { - const pickText = (/** @type {Node | null} */ el) => String(el?.textContent ?? '').trim(); - return nodes.map(n => { - /** @type {Node} */ - const titleElement = n.querySelector('.expand-control'); - const title = pickText(titleElement?.childNodes?.[0] ?? null); - const date = pickText(n.querySelector('.expand-control span')) - .replace(/^([A-Z][a-z]{2})[a-z]*( \d+, \d+)$/, '$1$2'); - /** @type {Element[]} */ - const linkElements = Array.from(n.querySelectorAll('.downloads a[href]')); - const links = linkElements.map(a => ({ - text: pickText(a), - href: a.getAttribute('href') ?? '', - })); - - /** @type {{ [filename: string]: string }} */ - const checksums = {}; - /** @type {HTMLElement} */ - const downloadsElement = n.querySelector('.downloads'); - const bodyText = (downloadsElement?.innerText || '').trim(); - bodyText.split('\n').forEach(line => { - const m = /^\s*([a-f0-9]{64})\s+(.+?)\s*$/.exec(line); - if (m) { - const [ _, sha256, filename ] = m; - checksums[filename] = sha256; - } - }); - - /* e.g. "2025.1.3". */ - const version = title.match(/\d{2,}\.\d+(?:\.\d+)?/)?.[0] ?? null; - return { title, date, version, links, checksums }; - }); - }); - - await browser.close(); - - return archives; +/** + * @example string + * 'September 26, 2025' -> 'Sep 26, 2025' + * @param {string} date + */ +export function formatDate(date) { + return date.replace(/([A-Z][a-z]{2})\w* (\d+), (\d+)/, '$1 $2, $3'); } async function main() { const props = readPropertiesSync(); - const minSupportedAndroidStudioVersion = props['MIN_SUPPORTED_ANDROID_STUDIO_IDE_VERSION']; - const getVersionFromTitle = (/** @type {string} */ title) => title.split('|')[1].trim(); - const archives = await getAndroidStudioArchives(); - const results = archives.filter(archive => { - if (!archive.title.includes('|')) return false; - return compareVersionStrings(archive.version ?? '0', minSupportedAndroidStudioVersion) >= 0; - }).map(({ title, date, version, links }) => { - const windowsZipUrl = links.find(link => link.text.match(/-windows(-exe)?\.zip/i))?.href; + const minSupportedAndroidStudioVersion = props.get('MIN_SUPPORTED_ANDROID_STUDIO_IDE_VERSION'); + const releases = await getAndroidStudioReleases(); + const results = releases.filter(release => { + return compareVersionStrings(release.version, minSupportedAndroidStudioVersion) >= 0; + }).map(({ name, date, version, download, channel }) => { + const windowsZipUrl = download.find(e => e.link.match(/-windows(-exe)?\.zip/i))?.link; if (!windowsZipUrl) { console.log('Unable to find Windows zip link for:'); - console.log(links.map(link => link.text).join('\n')); + console.log(download.map(e => e.link).join('\n')); } - const stable = isVersionStable(getVersionFromTitle(title)) || '-'; - return { title, date, version, stable, 'link for Windows (zip)': windowsZipUrl }; + const stable = /Release|Patch/i.test(channel) || '-'; + return { name, date: formatDate(date), version, stable, 'link for Windows (zip)': windowsZipUrl }; }).sort((a, b) => { - return compareVersionStrings(getVersionFromTitle(b.title), getVersionFromTitle(a.title)); + return compareVersionStrings(b.version, a.version); }); console.table(results); } diff --git a/.utils/fetch-and-parse-android-studio-latest-stable-version.mjs b/.utils/fetch-and-parse-android-studio-latest-stable-version.mjs deleted file mode 100644 index e03d2cfd..00000000 --- a/.utils/fetch-and-parse-android-studio-latest-stable-version.mjs +++ /dev/null @@ -1,122 +0,0 @@ -// fetch-and-parse-android-studio-latest-stable-version.mjs - -import * as cheerio from 'cheerio'; -import { bytes2GiB } from './utils/format.mjs'; -import { fileURLToPath } from 'node:url'; -import { getRemoteFileSizeBytes } from './utils/fetch.mjs'; - -const URL = 'https://developer.android.com/studio?hl=en'; - -/** - * @param {string} s - * @returns {string} - */ -const normalize = (s) => (s ?? '').replace(/\s+/g, ' ').trim(); - -/** - * @param {string} filename - * @param {string} kind - * @returns {string | null} - */ -function buildDownloadUrlFromFilename(filename, kind) { - const m = /android-studio-([\d.]+)-/.exec(filename); - if (!m) return null; - const version = m[1]; - const urlType = { - 'exe': 'install', - 'zip': 'ide-zips', - 'tar': 'ide-zips', - }[kind]; - if (!urlType) { - throw new Error(`Unsupported kind: ${kind}`); - } - return `https://redirector.gvt1.com/edgedl/android/studio/${urlType}/${version}/${filename}`; -} - -/** - * @param {string} url - * @returns {Promise} - */ -async function fetchHtml(url) { - const res = await fetch(url, { - headers: { - 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36', - 'accept-language': 'en', - }, - }); - if (!res.ok) throw new Error(`HTTP ${res.status} for ${url}`); - return await res.text(); -} - -/** - * @param {string} html - * @returns {Promise} - */ -async function parseItemsFromHtml(html) { - const $ = cheerio.load(html); - const rows = []; - - /** @type {import('cheerio').Cheerio} */ - const tableRows = $('table.download tbody tr'); - await Promise.all(Array.from(tableRows).map(async tr => { - const tds = $(tr).find('td'); - if (tds.length !== 4) return; - - const platform = normalize($(tds[0]).text()); - if (!/windows|linux/i.test(platform)) return; - - const btn = $(tds[1]).find('button.devsite-dialog-button').first(); - const filename = normalize(btn.text()); - if (!filename || !filename.includes('android-studio')) return; - - const sha256 = normalize($(tds[3]).text()); - - // @formatter:off - const kind = filename.match(/\.exe$/) ? 'exe' - : filename.match(/\.zip$/) ? 'zip' - : filename.match(/\.tar(\.gz)?$/) ? 'tar' - : 'other'; - // @formatter:on - - const url = buildDownloadUrlFromFilename(filename, kind); - const bytes = await getRemoteFileSizeBytes(url); - const size = bytes2GiB(bytes); - rows.push({ platform, filename, size, sha256, url, kind }); - })) - return rows - .filter(r => r.kind === 'exe' || r.kind === 'zip' || r.kind === 'tar') - .sort((_, b) => b.kind === 'exe' ? 1 : b.kind === 'zip' ? 1 : -1); -} - -/** - * Export: Get download information for the latest stable archives (exe, zip, tar)
- * Returns array like: [ { platform, filename, size, sha256, url, kind: 'exe'|'zip'|'tar' } ]
- * zh-CN:
- * 导出: 获取最新稳定版档案的下载信息 (exe, zip, tar)
- * 返回形如: [ { platform, filename, size, sha256, url, kind: 'exe'|'zip'|'tar' } ] - * - * @param {string} [sourceUrl=URL] - * @returns {Promise} - */ -export async function getLatestStableArchives(sourceUrl = URL) { - const html = await fetchHtml(sourceUrl); - const items = await parseItemsFromHtml(html); - if (!items.length) { - throw new Error('No latest stable archives found in the page'); - } - return items; -} - -async function main() { - const items = await getLatestStableArchives(URL); - console.table(items.map(({ filename, size, url, sha256 }) => ({ filename, size, url, sha256 }))); -} - -// Determine if this file is being run directly. -// zh-CN: 判断是否为直接执行该文件. -if (fileURLToPath(import.meta.url) === process.argv[1]) { - main().catch(err => { - console.error(err); - process.exitCode = 1; - }); -} diff --git a/.utils/run-scrapers.mjs b/.utils/run-scrapers.mjs index b1db374f..c85b626c 100644 --- a/.utils/run-scrapers.mjs +++ b/.utils/run-scrapers.mjs @@ -9,15 +9,15 @@ import { spawn } from 'node:child_process'; const SCRIPT_LIST = [ 'scrape-and-update-readme-template-contributors-table.mjs', 'scrape-and-update-foojay-resolver-version.mjs', - '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', - 'scrape-and-inject-gradle-kotlin-compatibility-list.mjs', - 'scrape-and-inject-ksp-releases.mjs', - 'scrape-and-inject-agp-gradle-compatibility-list.mjs', - 'scrape-and-inject-java-gradle-compatibility-list.mjs', 'scrape-and-inject-rhino-engine-data.mjs', + 'scrape-and-inject-latest-gradle-wrapper.mjs', + 'scrape-and-inject-gradle-kotlin-compatibility-map.mjs', + 'scrape-and-inject-android-studio-codename-maps.mjs', + 'scrape-and-inject-android-studio-agp-version-map.mjs', + 'scrape-and-inject-java-gradle-compatibility-map.mjs', + 'scrape-and-inject-agp-gradle-compatibility-map.mjs', + 'scrape-and-inject-agp-releases-list.mjs', + 'scrape-and-inject-ksp-releases-map.mjs', ]; const childProcessOutput = []; diff --git a/.utils/scrape-and-inject-agp-gradle-compatibility-list.mjs b/.utils/scrape-and-inject-agp-gradle-compatibility-map.mjs similarity index 67% rename from .utils/scrape-and-inject-agp-gradle-compatibility-list.mjs rename to .utils/scrape-and-inject-agp-gradle-compatibility-map.mjs index 0783c8ad..e4bd665f 100644 --- a/.utils/scrape-and-inject-agp-gradle-compatibility-list.mjs +++ b/.utils/scrape-and-inject-agp-gradle-compatibility-map.mjs @@ -1,9 +1,9 @@ -// scrape-and-inject-agp-gradle-compatibility-list.mjs +// scrape-and-inject-agp-gradle-compatibility-map.mjs import { compareVersionStrings } from './utils/versioning.mjs'; import { findTargetRows } from './utils/puppeteer-helpers.mjs'; import { getMinSupportedAgpVersion, getMinSupportedGradleVersion } from './utils/properties.mjs'; -import { updateAnchoredListInFile } from './utils/anchors.mjs'; +import { updateGradleMapData } from './utils/update-helper.mjs'; const URL = 'https://developer.android.com/build/releases/gradle-plugin#updating-gradle'; @@ -26,20 +26,15 @@ const URL = 'https://developer.android.com/build/releases/gradle-plugin#updating }); const minSupportedAgpVersion = getMinSupportedAgpVersion(); const minSupportedGradleVersion = getMinSupportedGradleVersion(); - const map = {}; + const map = new Map(); for (const { pluginVersion, gradleVersion } of rows) { if (compareVersionStrings(pluginVersion, minSupportedAgpVersion) < 0) continue; if (compareVersionStrings(gradleVersion, minSupportedGradleVersion) < 0) continue; - map[pluginVersion] = gradleVersion; + map.set(pluginVersion, gradleVersion); } - - await updateAnchoredListInFile('../settings.gradle.kts', { - anchorTag: 'AGP_GRADLE_COMPATIBILITY_LIST', - listName: 'agpGradleCompatibility', - lines: Object.entries(map) - .sort((a, b) => compareVersionStrings(b[0], a[0])) - .map(([ pluginVersion, gradleVersion ]) => `"${pluginVersion}" to "${gradleVersion}",`), - updatedLabel: 'AGP and Gradle compatibility list', + await updateGradleMapData('agp-gradle-compat', map, { + label: 'AGP and Gradle compatibility map', + sort: 'key.descending.as.version', }); })().catch(err => { console.error(err); diff --git a/.utils/scrape-and-inject-agp-releases.mjs b/.utils/scrape-and-inject-agp-releases-list.mjs similarity index 53% rename from .utils/scrape-and-inject-agp-releases.mjs rename to .utils/scrape-and-inject-agp-releases-list.mjs index ec230fd7..69e1dbfe 100644 --- a/.utils/scrape-and-inject-agp-releases.mjs +++ b/.utils/scrape-and-inject-agp-releases-list.mjs @@ -1,9 +1,9 @@ -// scrape-and-inject-agp-releases.mjs +// scrape-and-inject-agp-releases-list.mjs import * as cheerio from 'cheerio'; -import { compareVersionStrings, compareVersionStringsDescending } from './utils/versioning.mjs'; +import { compareVersionStrings } from './utils/versioning.mjs'; import { getMinSupportedAgpVersion } from './utils/properties.mjs'; -import { updateAnchoredListInFile } from './utils/anchors.mjs'; +import { updateGradleListData } from './utils/update-helper.mjs'; const URL = 'https://developer.android.com/reference/tools/gradle-api'; @@ -19,14 +19,10 @@ const URL = 'https://developer.android.com/reference/tools/gradle-api'; } }); const minSupportedVersion = getMinSupportedAgpVersion(); - const agpList = Array.from(results) - .filter(v => compareVersionStrings(v, minSupportedVersion) >= 0) - .sort(compareVersionStringsDescending); - await updateAnchoredListInFile('../settings.gradle.kts', { - anchorTag: 'ANDROID_GRADLE_PLUGIN_RELEASES_LIST', - listName: 'agpReleases', - lines: agpList.map(v => `"${v}",`), - updatedLabel: 'AGP releases list', + const agpList = new Set(Array.from(results).filter(v => compareVersionStrings(v, minSupportedVersion) >= 0)); + await updateGradleListData('agp-releases', agpList, { + label: 'AGP releases list', + sort: 'descending.as.version', }); })().catch(err => { console.error('Failed to fetch AGP releases:', err); diff --git a/.utils/scrape-and-inject-android-studio-agp-version-map.mjs b/.utils/scrape-and-inject-android-studio-agp-version-map.mjs index 8871bdd5..6bc2ea51 100644 --- a/.utils/scrape-and-inject-android-studio-agp-version-map.mjs +++ b/.utils/scrape-and-inject-android-studio-agp-version-map.mjs @@ -1,39 +1,36 @@ -// scrape-android-studio-agp_version_maps.mjs +// scrape-and-inject-android-studio-agp-version-map.mjs import { compareVersionStrings } from './utils/versioning.mjs'; import { fetchStudioAgpTable } from './fetch-and-parse-android-studio-agp-compatibility-table.mjs'; import { readPropertiesSync } from './utils/properties.mjs'; -import { updateAnchoredMapInFile } from './utils/anchors.mjs'; +import { updateGradleMapData } from './utils/update-helper.mjs'; const props = readPropertiesSync(); const version = { - MIN_IDE: props['MIN_SUPPORTED_ANDROID_STUDIO_IDE_VERSION'], - MIN_AGP: props['MIN_SUPPORTED_ANDROID_STUDIO_AGP_VERSION'], + MIN_IDE: props.get('MIN_SUPPORTED_ANDROID_STUDIO_IDE_VERSION'), + MIN_AGP: props.get('MIN_SUPPORTED_ANDROID_STUDIO_AGP_VERSION'), }; (async function main() { const agpTable = await fetchStudioAgpTable(); - /** @type {{ [targetStudioVersion: string]: string }} */ - const agpMap = {}; + const map = new Map(); for (const { studioVersion, agpRange } of agpTable) { const targetStudioVersion = studioVersion.match(/\d{2,}\.\d+\.\d+/)?.[0]; if (!targetStudioVersion) continue; const [ _, targetAgpVersion ] = agpRange.split('-'); - if (!(targetStudioVersion in agpMap) || compareVersionStrings(agpMap[targetStudioVersion], targetAgpVersion) > 0) { - agpMap[targetStudioVersion] = targetAgpVersion; + if (!map.has(targetStudioVersion) || compareVersionStrings(map.get(targetStudioVersion), targetAgpVersion) > 0) { + map.set(targetStudioVersion, targetAgpVersion); } if (compareVersionStrings(targetStudioVersion, version.MIN_IDE) <= 0) break; if (compareVersionStrings(targetAgpVersion, version.MIN_AGP) <= 0) break; } - await updateAnchoredMapInFile('../settings.gradle.kts', { - anchorTag: 'ANDROID_STUDIO_AGP_VERSION_MAP', - mapName: 'agpVersionMap', - lines: Object.entries(agpMap).map(([ studioVer, agpVer ]) => `"${studioVer}" to "${agpVer}",`), - updatedLabel: 'AGP version map', + await updateGradleMapData('android-studio-agp-compat', map, { + label: 'Android Studio and AGP compatibility map', + sort: 'key.descending.as.version', }); })().catch(err => { console.error(err); diff --git a/.utils/scrape-and-inject-android-studio-codename-maps.mjs b/.utils/scrape-and-inject-android-studio-codename-maps.mjs new file mode 100644 index 00000000..afdf37cf --- /dev/null +++ b/.utils/scrape-and-inject-android-studio-codename-maps.mjs @@ -0,0 +1,563 @@ +// scrape-and-inject-android-studio-codename-maps.mjs + +import * as fsp from 'node:fs/promises'; +import * as path from 'node:path'; +import { compareVersionStrings, compareVersionStringsDescending } from './utils/versioning.mjs'; +import { extractLatestStableRelease, getAndroidStudioReleases, refineDownloadItemsWithRealSize } from './fetch-and-parse-android-studio-archives.mjs'; +import { toUpdatedStamp, toYYYYMMDD } from './utils/date.mjs'; +import { readPropertiesSync } from './utils/properties.mjs'; +import { updateGradleLinesData, updateGradleMapData } from './utils/update-helper.mjs'; + +/** + * 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 letter 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 MIN_ANDROID_STUDIO_VERSION = (/* @IIFE */ () => { + const props = readPropertiesSync('../version.properties'); + return props.get('MIN_SUPPORTED_ANDROID_STUDIO_IDE_VERSION'); +})(); + +/** + * Update common.json with the lastest stable release.
+ * zh-CN: 用最新稳定版更新 common.json. + * + * @param {AndroidStudioReleaseItem[]} releases + * @returns {Promise} + */ +async function updateCommonDataByLatestRelease(releases) { + const latest = extractLatestStableRelease(releases); + const latestDownload = await refineDownloadItemsWithRealSize(latest.download, /windows|linux/i); + + /** + * @param {RegExp} suffix + * @returns {{ name: string, link: string, size: string }} + */ + const pickWinItem = (suffix) => { + const aim = latestDownload.find(l => suffix.test(l.link)); + if (!aim) throw new Error(`Matched archive entry missing Windows EXE/ZIP or Linux TAR download information (suffix: ${suffix})`); + return { name: aim.link.split('/').pop(), link: aim.link, size: aim.size }; + }; + const exeItem = pickWinItem(/\.exe$/); + const zipItem = pickWinItem(/\.zip$/); + const tarItem = pickWinItem(/\.tar(\.gz)?$/); + + // Prepare fields needed to write back to common.json. + // zh-CN: 准备写回 common.json 所需字段. + + const latestVersionName = latest.name; // e.g. "Android Studio Narwhal Feature Drop | 2025.1.2" + const latestVersionDate = toYYYYMMDD(latest.date) || ''; // e.g. "2025/07/31" + + // 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 fsp.readFile(commonJsonPath, 'utf8'); + const commonObj = JSON.parse(commonRaw); + + const updatedCommon = { + ...commonObj, + android_studio_latest_recommended_version_name: latestVersionName, + var_date_android_studio_latest_recommended_version_name: latestVersionDate, + android_studio_latest_recommended_file_name_of_exe: exeItem.name, + android_studio_latest_recommended_download_address_of_exe: exeItem.link, + android_studio_latest_recommended_file_size_of_exe: exeItem.size, + android_studio_latest_recommended_file_name_of_zip: zipItem.name, + android_studio_latest_recommended_download_address_of_zip: zipItem.link, + android_studio_latest_recommended_file_size_of_zip: zipItem.size, + android_studio_latest_recommended_file_name_of_tar: tarItem.name, + android_studio_latest_recommended_download_address_of_tar: tarItem.link, + android_studio_latest_recommended_file_size_of_tar: tarItem.size, + }; + + if (JSON.stringify(updatedCommon) !== JSON.stringify(commonObj)) { + await fsp.writeFile(commonJsonPath, JSON.stringify(updatedCommon, null, 2), 'utf8'); + console.log('[common.json] Updated (Android Studio information)'); + const from = commonObj.android_studio_latest_recommended_version_name; + const to = updatedCommon.android_studio_latest_recommended_version_name; + if (from !== to) { + const maxLength = Math.max(...[ from, to ].map(s => s.length + 5)); + const SEP_EQ = '='.repeat(maxLength); + console.log(SEP_EQ); + console.log(`-- "${from}"`); + console.log(`-> "${to}"`); + console.log(SEP_EQ); + } + } else { + // console.log('[common.json] No update needed (Android Studio information)'); + } +} + +/** + * Summarize codename-to-version mappings and codename first release dates.
+ * zh-CN: 汇总代号版本映射以及代号的首发日期. + * + * @param {AndroidStudioReleaseItem[]} releases + */ +function getCodenameMapLinesInfo(releases) { + + /** + * Parse "codename" from name, + * zh-CN: 从名称中解析 "代号", + * + * @example string + * 'Android Studio Meerkat Feature Drop | 2024.3.2 RC 1' -> 'Meerkat' + * + * @param {string} name + * @returns {string | null} + */ + const codenameFromName = (name) => { + // 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(name); + return m ? m[1].trim() : null; + }; + + /** + * 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} + */ + const buildUniquePrefixes = (names, overrides) => { + + /** + * @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 ])); + // 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; + if (!bucket.has(key)) bucket.set(key, []); + bucket.get(key).push(idx); + }); + + 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) { + const lockedIndices = indices.filter(i => entries[i].locked); + if (lockedIndices.length >= 2) { + const groupNames = indices.map(i => entries[i].name); + throw new Error(`[CodenameMap] Manual override mapping conflict: "${code}" -> [ ${groupNames.join(', ')} ]`); + } + for (const i of indices) { + const e = entries[i]; + if (e.locked) continue; + const maxLen = maxLenByName.get(e.name); + if (e.len >= maxLen) { + 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); + } + } + } + + return new Map(entries.map(e => [ e.name, e.code ])); + }; + + const codenames = [ ...new Set(releases.map(o => codenameFromName(o.name)).filter(Boolean)) ]; + const nameToCode = buildUniquePrefixes(codenames, manualCodenameOverrides); + + /** + * @example Map + * Map(22) { + * ... ... + * '2025.1.4.7' => 'N', + * '2025.1.4.6' => 'N', + * '2025.1.4.5' => 'N', + * '2025.1.4.4' => 'N', + * '2025.1.3.7' => 'N', + * '2025.1.4.3' => 'N', + * '2025.1.3.6' => 'N', + * '2025.1.2.13' => 'N', + * '2025.1.4.2' => 'N', + * '2025.1.2.12' => 'N', + * '2025.1.4.1' => 'N', + * '2025.1.3.5' => 'N', + * '2025.1.3.4' => 'N', + * ... ... + * } + * @type {Map} + */ + const versionToLetter = new 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(); + + const buildVersionMap = new Map(); + + for (const release of releases) { + if (!release.version) continue; + + if (compareVersionStrings(release.version, MIN_ANDROID_STUDIO_VERSION) >= 0) { + buildVersionMap.set(release.build.replace(/[^\d.]*/g, ''), release.version); + } + + const cname = codenameFromName(release.name); + if (!cname) continue; + + const code = nameToCode.get(cname); + if (!code) continue; + + if (versionToLetter.has(release.version)) { + if (versionToLetter.get(release.version) !== code) { + throw new Error(`[CodenameMap] Duplicate version: ${release.version}`); + } + } else { + versionToLetter.set(release.version, code); + } + + const d = new Date(release.date); + const existed = letterBorn.get(code); + if (!existed || d < existed.born) { + letterBorn.set(code, { name: cname, born: d }); + } + } + + const sortedVersions = Array.from(versionToLetter.keys()).sort(compareVersionStringsDescending); + + /** + * @example Array<[version, prefixCode]> + * [ + * ... .. + * [ '2025.1.4.7', 'N' ], + * [ '2025.1.4.6', 'N' ], + * [ '2025.1.4.5', 'N' ], + * [ '2025.1.4.4', 'N' ], + * [ '2025.1.4.3', 'N' ], + * [ '2025.1.4.2', 'N' ], + * [ '2025.1.4.1', 'N' ], + * [ '2025.1.3.7', 'N' ], + * [ '2025.1.3.6', 'N' ], + * [ '2025.1.3.5', 'N' ], + * [ '2025.1.3.4', 'N' ], + * [ '2025.1.3.3', 'N' ], + * [ '2025.1.3.2', 'N' ], + * [ '2025.1.3.1', 'N' ], + * ... ... + * ] + * @type {Array<[string, string]>} + */ + const versionLetterList = sortedVersions.map(v => [ v, versionToLetter.get(v) ]); + + /** + * @example { majorSeries: { revision: letter } } + * { + * ... ... + * '2025.1.4': { '1': 'N', '2': 'N', '3': 'N', ..., '6': 'N', '7': 'N' }, + * '2025.1.3': { '1': 'N', '2': 'N', '3': 'N', ..., '6': 'N', '7': 'N' }, + * '2025.1.2': { '1': 'N', '2': 'N', '3': 'N', ..., '6': 'N', '7': 'N', ..., '13': 'N' }, + * '2025.1.1': { '1': 'N', '2': 'N', '3': 'N', ..., '6': 'N', '7': 'N', ..., '14': 'N' }, + * '2024.3.2': { '1': 'M', '2': 'M', '3': 'M', ..., '6': 'M', '7': 'M', ..., '15': 'M' }, + * '2024.3.1': { '1': 'M', '2': 'M', '3': 'M', ..., '6': 'M', '7': 'M', ..., '15': 'M' }, + * '2024.2.2': { '1': 'L', '2': 'L', '3': 'L', ..., '6': 'L', '7': 'L', ..., '15': 'L' }, + * '2024.2.1': { '1': 'L', '2': 'L', '3': 'L', ..., '6': 'L', '7': 'L', ..., '12': 'L' }, + * '2024.1.3': { '1': 'L', '2': 'L', '3': 'L' }, + * '2024.1.2': { '1': 'K', '2': 'K', '3': 'K', ..., '6': 'K', '7': 'K', ..., '13': 'K' }, + * '2024.1.1': { '1': 'K', '2': 'K', '3': 'K', ..., '6': 'K', '7': 'K', ..., '13': 'K' }, + * '2023.3.2': { '1': 'J', '2': 'K' }, + * '2023.3.1': { '1': 'J', '2': 'J', '3': 'J', ..., '6': 'J', '7': 'J', ..., '20': 'J' }, + * '2023.2.1': { '1': 'I', '2': 'I', '3': 'I', ..., '6': 'I', '7': 'I', ..., '25': 'I' }, + * '2023.1.1': { '1': 'H', '2': 'H', '3': 'H', ..., '6': 'H', '7': 'H', ..., '28': 'H' }, + * '2022.3.1': { '1': 'G', '2': 'G', '3': 'G', ..., '6': 'G', '7': 'G', ..., '22': 'G' }, + * ... ... + * } + * @type {Object} + */ + const rawFirstlyVersionLetterMap = {}; + for (let i = 0; i < versionLetterList.length; i++) { + const [ v, letter ] = versionLetterList[i]; + const matched = v.match(/(^\d+\.\d+\.\d+)\.(\d+)/); + if (!matched) continue; + const [ , majorSeries, revision ] = matched; + if (majorSeries in rawFirstlyVersionLetterMap) { + rawFirstlyVersionLetterMap[majorSeries][revision] = letter; + } else { + rawFirstlyVersionLetterMap[majorSeries] = { [revision]: letter }; + } + } + + /** + * When all versions with the same major series prefix (like `2025.1.x.x`) + * point to the same codename prefix (like `'N'`), + * they can be merged (like `{ '2025.1.4' : 'N' }`, where `2025.1.4` is the major series prefix), + * otherwise retain the original split form (like `2023.3.2.x` cannot be merged).
+ * zh-CN:
+ * 当主版本系列相同的版本 (如 `2025.1.x.x`) 全部指向同一个代号前缀 (如 `'N'`) 时, + * 可进行合并 (如 `{ '2025.1.4' : 'N' }`, 其中 `2025.1.4` 为主版本系列), + * 否则保留原始的拆分形式 (如 `2023.3.2.x` 不可合并). + * @example { version: letter } + * { + * ... ... + * '2025.1.4': 'N', + * '2025.1.3': 'N', + * '2025.1.2': 'N', + * '2025.1.1': 'N', + * '2024.3.2': 'M', + * '2024.3.1': 'M', + * '2024.2.2': 'L', + * '2024.2.1': 'L', + * '2024.1.3': 'L', + * '2024.1.2': 'K', + * '2024.1.1': 'K', + * '2023.3.2.1': 'J', + * '2023.3.2.2': 'K', + * '2023.3.1': 'J', + * '2023.2.1': 'I', + * '2023.1.1': 'H', + * '2022.3.1': 'G', + * ... ... + * } + * @type {Object<[version: string], string>} + */ + const combinedFirstlyVersionLetterMap = {}; + + Object.entries(rawFirstlyVersionLetterMap).forEach(([ majorSeries, revisionToLetter ]) => { + const letterValues = Object.values(revisionToLetter); + if (new Set(letterValues).size === 1) { + /* Combine. (zh-CN: 合并.) */ + combinedFirstlyVersionLetterMap[majorSeries] = letterValues[0]; + } else { + /* Keep expanded. (zh-CN: 保持展开.) */ + Object.entries(revisionToLetter).forEach(([ revision, letter ]) => { + combinedFirstlyVersionLetterMap[`${majorSeries}.${revision}`] = letter; + }); + } + }); + + /** + * @example { majorSeries: { patch: letter } } + * { + * ... ... + * '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.2': { '1': 'I' }, + * '2023.1': { '1': 'H' }, + * '2022.3': { '1': 'G' }, + * ... ... + * } + * @type {Object} + */ + const rawVersionLetterMap = {}; + /** + * @example + * Set(1) { '2023.3' } + * @type {Set} + */ + const excludedVersionSet = new Set(); + /** + * @example + * { + * '2023.3.2.1': 'J', + * '2023.3.2.2': 'K', + * '2023.3.1': 'J', + * } + * @type {Object} + */ + const excludedVersionLetterMap = {}; + const entries = Object.entries(combinedFirstlyVersionLetterMap).filter(([ v, letter ]) => { + if (v.split('.').length > 3) { + excludedVersionLetterMap[v] = letter; + excludedVersionSet.add(v.split('.').slice(0, 2).join('.')); + return false; + } + return true; + }); + for (let i = 0; i < entries.length; i++) { + const [ v, letter ] = entries[i]; + if (excludedVersionSet.has(v.split('.').slice(0, 2).join('.'))) { + excludedVersionLetterMap[v] = letter; + continue; + } + const matched = v.match(/(^\d+\.\d+)\.(\d+)/); + if (!matched) continue; + const [ , majorSeries, patch ] = matched; + if (majorSeries in rawVersionLetterMap) { + rawVersionLetterMap[majorSeries][patch] = letter; + } else { + rawVersionLetterMap[majorSeries] = { [patch]: letter }; + } + } + + /** + * When all versions with the same major 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 major series prefix), + * otherwise retain the original split form (like `2024.1` cannot be merged).
+ * zh-CN:
+ * 当主版本系列相同的版本 (如 `2025.1.x`) 全部指向同一个代号前缀 (如 `'N'`) 时, + * 可进行合并 (如 `{ '2025.1' : 'N' }`, 其中 `2025.1` 为主版本系列), + * 否则保留原始的拆分形式 (如 `2024.1` 不可合并). + * @example { version: letter } + * { + * ... ... + * '2025.1.4': 'N', + * '2025.1.3': 'N', + * '2025.1.2': 'N', + * '2025.1.1': 'N', + * '2024.3.2': 'M', + * '2024.3.1': 'M', + * '2024.2.2': 'L', + * '2024.2.1': 'L', + * '2024.1.3': 'L', + * '2024.1.2': 'K', + * '2024.1.1': 'K', + * '2023.3.2.1': 'J', + * '2023.3.2.2': 'K', + * '2023.3.1': 'J', + * '2023.2.1': 'I', + * '2023.1.1': 'H', + * '2022.3.1': 'G', + * ... ... + * } + * @type {Object<[version: string], string>} + */ + const combinedVersionLetterMap = structuredClone(excludedVersionLetterMap); + + Object.entries(rawVersionLetterMap).forEach(([ majorSeries, patchToLetter ]) => { + const letterValues = Object.values(patchToLetter); + if (new Set(letterValues).size === 1) { + /* Combine. (zh-CN: 合并.) */ + combinedVersionLetterMap[majorSeries] = letterValues[0]; + } else { + /* Keep expanded. (zh-CN: 保持展开.) */ + Object.entries(patchToLetter).forEach(([ patch, letter ]) => { + combinedVersionLetterMap[`${majorSeries}.${patch}`] = letter; + }); + } + }); + + const codenameVersionMap = new Map(Object.entries(combinedVersionLetterMap)); + + const codenameLines = Array.from(letterBorn.entries()) + .sort((a, b) => { + return new Date(b[1].born).getTime() - new Date(a[1].born).getTime(); + }) + .map(([ code, { name, born } ]) => { + const bornStr = toUpdatedStamp(born); + // e.g. [ "#Born on Nov 12, 2024", "M=Meerkat" ] + return [ `#Born on ${bornStr}`, `${code}=${name}` ]; + }) + .flat(1); + + return { buildVersionMap, codenameVersionMap, codenameLines }; +} + +(async function main() { + const releases = await getAndroidStudioReleases(); + await updateCommonDataByLatestRelease(releases); + + const { buildVersionMap, codenameVersionMap, codenameLines } = getCodenameMapLinesInfo(releases); + + await updateGradleMapData('android-studio-build-version', buildVersionMap, { + label: 'Android Studio build version map', + sort: 'value.descending.as.version', + }); + + await updateGradleMapData('android-studio-codename-version', codenameVersionMap, { + label: 'Android Studio codename version map', + sort: 'key.descending.as.version', + }); + + await updateGradleLinesData('android-studio-codename', codenameLines, { + label: 'Android Studio codename map', + }); +})().catch(err => { + console.error(err); + process.exitCode = 1; +}); diff --git a/.utils/scrape-and-inject-android-studio-codename_maps.mjs b/.utils/scrape-and-inject-android-studio-codename_maps.mjs deleted file mode 100644 index d39bedc6..00000000 --- a/.utils/scrape-and-inject-android-studio-codename_maps.mjs +++ /dev/null @@ -1,467 +0,0 @@ -// scrape-and-inject-android-studio-codename_maps.mjs - -import * as fsp from 'node:fs/promises'; -import * as path from 'node:path'; -import { batchUpdateAnchoredBlocks } from './utils/anchors.mjs'; -import { bytes2GiB } from './utils/format.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'; - -/** - * 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 = {}; - -/** - * Use the latest stable version's checksum/filename to locate the entry in archives, - * complete and update common.json.
- * zh-CN: 用 "最新稳定版" 的校验和/文件名, 在归档中定位条目, 补全并更新 common.json. - * - * @param {AndroidStudioArchiveItem[]} 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('Latest stable archives missing required "kind" info: exe, zip, or tar'); - } - - /** - * Search the archive: first try to match by sha256, then by filename.
- * zh-CN: 在归档中查找: 优先用 sha256 命中, 其次用文件名. - * - * @param {AndroidStudioArchiveItem[]} rows - * @param {AndroidStudioStableArchiveItem} target - * @returns {AndroidStudioArchiveItem | null} - */ - const matchArchive = (rows, target) => { - for (const arc of rows) { - const bySha = target.sha256 && arc.checksums[target.filename] === target.sha256; - const byName = arc.links.some(l => l.text === target.filename); - if (bySha || byName) return arc; - } - return null; - }; - const matchedArc = matchArchive(archives, latestExe) || matchArchive(archives, latestZip); - if (!matchedArc) { - throw new Error('Could not locate entries in the archive corresponding to the latest version (neither by sha256 nor filename)'); - } - - /** - * @param {string} suffix - * @returns {{ filename: string, url: string, sizeGiB?: string | null } | null} - */ - const pickWinItem = (suffix) => { - const link = matchedArc.links.find(l => l.text.endsWith(suffix)); - if (!link) return null; - return { filename: link.text, url: link.href }; - }; - const exeItem = pickWinItem('-windows.exe'); - const zipItem = pickWinItem('-windows.zip'); - const tarItem = pickWinItem('-linux.tar.gz'); - - 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), - tarItem ? getRemoteFileSizeBytes(tarItem.url) : Promise.resolve(null), - ]); - if (exeItem) exeItem.sizeGiB = bytes2GiB(exeBytes); - if (zipItem) zipItem.sizeGiB = bytes2GiB(zipBytes); - if (tarItem) tarItem.sizeGiB = bytes2GiB(tarBytes); - - // Prepare fields needed to write back to common.json. - // zh-CN: 准备写回 common.json 所需字段. - - 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" - - // 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 fsp.readFile(commonJsonPath, 'utf8'); - const commonObj = JSON.parse(commonRaw); - - const updatedCommon = { - ...commonObj, - android_studio_latest_recommended_version_name: latestVersionName, - var_date_android_studio_latest_recommended_version_name: latestVersionDate, - android_studio_latest_recommended_file_name_of_exe: exeItem.filename, - android_studio_latest_recommended_download_address_of_exe: exeItem.url, - android_studio_latest_recommended_file_size_of_exe: exeItem.sizeGiB ?? commonObj.android_studio_latest_recommended_file_size_of_exe, - android_studio_latest_recommended_file_name_of_zip: zipItem.filename, - android_studio_latest_recommended_download_address_of_zip: zipItem.url, - android_studio_latest_recommended_file_size_of_zip: zipItem.sizeGiB ?? commonObj.android_studio_latest_recommended_file_size_of_zip, - android_studio_latest_recommended_file_name_of_tar: tarItem.filename, - android_studio_latest_recommended_download_address_of_tar: tarItem.url, - android_studio_latest_recommended_file_size_of_tar: tarItem.sizeGiB ?? commonObj.android_studio_latest_recommended_file_size_of_tar, - }; - - if (JSON.stringify(updatedCommon) !== JSON.stringify(commonObj)) { - await fsp.writeFile(commonJsonPath, JSON.stringify(updatedCommon, null, 2), 'utf8'); - console.log('[common.json] Updated (Android Studio information)'); - const from = commonObj.android_studio_latest_recommended_version_name; - const to = updatedCommon.android_studio_latest_recommended_version_name; - if (from !== to) { - const maxLength = Math.max(...[ from, to ].map(s => s.length + 5)); - const SEP_EQ = '='.repeat(maxLength); - console.log(SEP_EQ); - console.log(`-- "${from}"`); - console.log(`-> "${to}"`); - console.log(SEP_EQ); - } - } else { - // console.log('[common.json] No update needed (Android Studio information)'); - } -} - -/** - * Summarize codename-to-version mappings and codename first release dates.
- * zh-CN: 汇总代号版本映射以及代号的首发日期. - * - * @param {AndroidStudioArchiveItem[]} archives - */ -function getCodenameMapLinesInfo(archives) { - - /** - * 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 = 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; - }; - - /** - * 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} - */ - const buildUniquePrefixes = (names, overrides) => { - - /** - * @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 ])); - // 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; - if (!bucket.has(key)) bucket.set(key, []); - bucket.get(key).push(idx); - }); - - 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) { - const lockedIndices = indices.filter(i => entries[i].locked); - if (lockedIndices.length >= 2) { - const groupNames = indices.map(i => entries[i].name); - throw new Error(`[CodenameMap] Manual override mapping conflict: "${code}" -> [ ${groupNames.join(', ')} ]`); - } - for (const i of indices) { - const e = entries[i]; - if (e.locked) continue; - const maxLen = maxLenByName.get(e.name); - if (e.len >= maxLen) { - 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); - } - } - } - - return new Map(entries.map(e => [ e.name, e.code ])); - }; - - const codenames = [ ...new Set(archives.map(o => codenameFromTitle(o.title)).filter(Boolean)) ]; - const nameToCode = buildUniquePrefixes(codenames, manualCodenameOverrides); - - /** - * @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(); - - /** - * @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; - const cname = codenameFromTitle(arc.title); - if (!cname) continue; - - const code = nameToCode.get(cname); - if (!code) continue; - - 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) { - letterBorn.set(code, { name: cname, born: d }); - } - } - - 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('|') ]); - - /** - * @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 [ , minorSeries, patch ] = matched; - if (minorSeries in rawVersionLettersMap) { - rawVersionLettersMap[minorSeries][patch] = letters; - } else { - rawVersionLettersMap[minorSeries] = { [patch]: letters }; - } - } - - /** - * 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(([ minorSeries, patchToLetters ]) => { - const letterValues = Object.values(patchToLetters); - if (new Set(letterValues).size === 1) { - /* Combine. (zh-CN: 合并.) */ - combinedVersionLettersMap[minorSeries] = letterValues[0]; - } else { - /* Keep expanded. (zh-CN: 保持展开.) */ - Object.entries(patchToLetters).forEach(([ patch, letters ]) => { - combinedVersionLettersMap[`${minorSeries}.${patch}`] = letters; - }); - } - }); - - const versionMapLines = Object.entries(combinedVersionLettersMap) - .sort((a, b) => compareVersionStrings(b[0], a[0])) - .map(([ v, letters ]) => `"${v}" to "${letters}",`); - - 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 codename version map', - }, { - type: 'map', - anchorTag: 'ANDROID_STUDIO_CODENAME_MAP', - mapName: 'codenameMap', - lines: codenameMapLines, - updatedLabel: 'Android Studio codename map', - } ]); -})().catch(err => { - console.error(err); - process.exitCode = 1; -}); diff --git a/.utils/scrape-and-inject-gradle-kotlin-compatibility-list.mjs b/.utils/scrape-and-inject-gradle-kotlin-compatibility-list.mjs deleted file mode 100644 index 1c720541..00000000 --- a/.utils/scrape-and-inject-gradle-kotlin-compatibility-list.mjs +++ /dev/null @@ -1,58 +0,0 @@ -// scrape-and-inject-gradle-kotlin-compatibility-list.mjs - -import { findTargetRows } from './utils/puppeteer-helpers.mjs'; -import { compareVersionStrings } from './utils/versioning.mjs'; -import { getMinSupportedGradleVersion } from './utils/properties.mjs'; -import { updateAnchoredListInFile } from './utils/anchors.mjs'; - -const URL = 'https://docs.gradle.org/current/userguide/compatibility.html#kotlin'; - -const unofficialKotlinCompatibilityList = { - // '8.14': '2.1.10', - // '8.13': '2.1.10', -}; - -(async function main() { - const rows = await findTargetRows({ - url: URL, - tableSelector: 'table.tableblock', - tableFilter: { - th: /Embedded Kotlin version|Minimum Gradle version|Kotlin Language version/i, - }, - tableDataStructure: [ - 'kotlin', - 'gradle', - 'ktLanguage', - ], - }); - /** - * @type {{ [gradle: string]: { kotlin: string, isUnofficial: boolean } }} - */ - const map = {}; - const minSupportedGradleVersion = getMinSupportedGradleVersion(); - - for (const { kotlin, gradle } of rows) { - if (compareVersionStrings(gradle, minSupportedGradleVersion) < 0) continue; - map[gradle] = { kotlin, isUnofficial: false }; - } - - Object.entries(unofficialKotlinCompatibilityList).forEach(([ gradle, kotlin ]) => { - if (!(gradle in map)) map[gradle] = { kotlin, isUnofficial: true }; - }); - - await updateAnchoredListInFile('../settings.gradle.kts', { - anchorTag: 'GRADLE_KOTLIN_COMPATIBILITY_LIST', - listName: 'gradleKotlinCompatibility', - lines: Object.entries(map) - .sort((a, b) => compareVersionStrings(b[1]['kotlin'], a[1]['kotlin'])) - .map(([ gradle, { kotlin, isUnofficial } ]) => { - return isUnofficial - ? `"${gradle}" to "${kotlin}", /* Unofficial. */` - : `"${gradle}" to "${kotlin}",`; - }), - updatedLabel: 'Gradle and Kotlin compatibility list', - }); -})().catch(err => { - console.error(err); - process.exitCode = 1; -}); diff --git a/.utils/scrape-and-inject-gradle-kotlin-compatibility-map.mjs b/.utils/scrape-and-inject-gradle-kotlin-compatibility-map.mjs new file mode 100644 index 00000000..fbf8e271 --- /dev/null +++ b/.utils/scrape-and-inject-gradle-kotlin-compatibility-map.mjs @@ -0,0 +1,48 @@ +// scrape-and-inject-gradle-kotlin-compatibility-map.mjs + +import { findTargetRows } from './utils/puppeteer-helpers.mjs'; +import { compareVersionStrings } from './utils/versioning.mjs'; +import { getMinSupportedGradleVersion } from './utils/properties.mjs'; +import { updateGradleMapData } from './utils/update-helper.mjs'; + +const URL = 'https://docs.gradle.org/current/userguide/compatibility.html#kotlin'; + +(async function main() { + const rows = await findTargetRows({ + url: URL, + tableSelector: 'table.tableblock', + tableFilter: { + th: /Embedded Kotlin version|Minimum Gradle version|Kotlin Language version/i, + }, + tableDataStructure: [ + 'kotlin', + 'gradle', + 'ktLanguage', + ], + }); + /** + * @example Map + * Map(10) { + * ... ... + * "8.12" => "2.0.21", + * "8.11" => "2.0.20", + * ... ... + * } + * @type {Map} + */ + const map = new Map(); + const minSupportedGradleVersion = getMinSupportedGradleVersion(); + + for (const { kotlin, gradle } of rows) { + if (compareVersionStrings(gradle, minSupportedGradleVersion) < 0) continue; + map.set(gradle, kotlin); + } + + await updateGradleMapData('gradle-kotlin-compat', map, { + label: 'Gradle and Kotlin compatibility map', + sort: 'value.descending.as.version', + }); +})().catch(err => { + console.error(err); + process.exitCode = 1; +}); diff --git a/.utils/scrape-and-inject-java-gradle-compatibility-list.mjs b/.utils/scrape-and-inject-java-gradle-compatibility-map.mjs similarity index 51% rename from .utils/scrape-and-inject-java-gradle-compatibility-list.mjs rename to .utils/scrape-and-inject-java-gradle-compatibility-map.mjs index 61288dd5..13e92c7e 100644 --- a/.utils/scrape-and-inject-java-gradle-compatibility-list.mjs +++ b/.utils/scrape-and-inject-java-gradle-compatibility-map.mjs @@ -1,15 +1,11 @@ -// scrape-and-inject-java-gradle-compatibility-list.mjs +// scrape-and-inject-java-gradle-compatibility-map.mjs import { findTargetRows } from './utils/puppeteer-helpers.mjs'; import { getMinSupportedJavaVersionInt } from './utils/properties.mjs'; -import { updateAnchoredListInFile } from './utils/anchors.mjs'; +import { updateGradleMapData } from './utils/update-helper.mjs'; const URL = 'https://docs.gradle.org/current/userguide/compatibility.html#java_runtime'; -const unofficialGradleCompatibilityList = { - // 25: '9.0', -}; - (async function main() { const rows = await findTargetRows({ url: URL, @@ -25,8 +21,7 @@ const unofficialGradleCompatibilityList = { { gradle: /^N\/A$|\d+\.\d+/ }, ], }); - /** @type {{ [javaInt: string]: string }} */ - const map = {}; + const map = new Map(); const minSupportedJavaVersionInt = getMinSupportedJavaVersionInt(); for (const { java, gradle } of rows) { const javaInt = parseInt(java); @@ -34,25 +29,12 @@ const unofficialGradleCompatibilityList = { throw Error(`Invalid java version int: "${java}"`); } if (javaInt >= minSupportedJavaVersionInt) { - map[javaInt] = gradle; + map.set(`${javaInt}`, gradle); } } - - await updateAnchoredListInFile('../settings.gradle.kts', { - anchorTag: 'JAVA_GRADLE_COMPATIBILITY_LIST', - listName: 'javaGradleCompatibility', - lines: Object.entries(map) - .sort((a, b) => Number(b[0]) - Number(a[0])) - .map(([ java, gradle ]) => { - if (gradle === 'N/A') { - const unofficialGradleVersion = unofficialGradleCompatibilityList[java]; - if (unofficialGradleVersion) { - return `${java} to "${unofficialGradleVersion}", /* Unofficial. */`; - } - } - return `${java} to "${gradle}",`; - }), - updatedLabel: 'Java and Gradle compatibility list', + await updateGradleMapData('java-gradle-compat', map, { + label: 'Java and Gradle compatibility map', + sort: 'key.descending.as.number', }); })().catch(err => { console.error(err); diff --git a/.utils/scrape-and-inject-ksp-releases.mjs b/.utils/scrape-and-inject-ksp-releases-map.mjs similarity index 79% rename from .utils/scrape-and-inject-ksp-releases.mjs rename to .utils/scrape-and-inject-ksp-releases-map.mjs index f1406648..c9e6c2e8 100644 --- a/.utils/scrape-and-inject-ksp-releases.mjs +++ b/.utils/scrape-and-inject-ksp-releases-map.mjs @@ -1,10 +1,10 @@ -// scrape-and-inject-ksp-releases.mjs +// scrape-and-inject-ksp-releases-map.mjs +// after: [ scrape-and-inject-gradle-kotlin-compatibility-map.mjs ] -import * as fsp from 'node:fs/promises'; import { httpFetch } from './utils/fetch.mjs'; -import { readProperties } from './utils/properties.mjs'; +import { readPropertiesSync } from './utils/properties.mjs'; import { toUpdatedStamp } from './utils/date.mjs'; -import { updateAnchoredMapInFile } from './utils/anchors.mjs'; +import { updateGradleLinesData } from './utils/update-helper.mjs'; const URL = 'https://api.github.com/repos/google/ksp/releases'; const GITHUB_TOKEN = process.env.GITHUB_TOKEN; @@ -65,26 +65,9 @@ async function fetchKspReleases() { } async function getMinKotlinVersionToCheck() { - const raw = await fsp.readFile('../settings.gradle.kts', 'utf8'); - const tag = 'GRADLE_KOTLIN_COMPATIBILITY_LIST'; - const beginIndex = raw.indexOf(`@AnchorBegin ${tag}`); - const endIndex = raw.indexOf(`@AnchorEnd ${tag}`); - - if (beginIndex !== -1 && endIndex !== -1) { - const props = await readProperties(); - const minGradle = props['MIN_SUPPORTED_GRADLE_VERSION']; - - const lines = raw.slice(beginIndex + tag.length + 1, endIndex).split('\n'); - for (const line of lines) { - const m = /^"(\d+(?:\.\d+)+)" to "(\d+(?:\.\d+)+)",/.exec(line.trim()); - if (m) { - const [ _, gradleVersion, kotlinVersion ] = m; - if (gradleVersion === minGradle) { - return kotlinVersion; - } - } - } - } + const minGradle = readPropertiesSync('../version.properties').get('MIN_SUPPORTED_GRADLE_VERSION'); + const mapElement = readPropertiesSync('../gradle/data/gradle-kotlin-compat.properties').get(minGradle); + if (mapElement) return mapElement; throw new Error(`Failed to find the minimum Kotlin version to check in settings.gradle.kts`); } @@ -175,17 +158,16 @@ function parseReleases(releases) { return b.date.getTime() - a.date.getTime(); }) .map(({ kotlinVer, kspVer, date }) => { - return `"${kotlinVer}" to "${kspVer}", /* ${(toUpdatedStamp(date))}. */`; - }); + return [ `#${toUpdatedStamp(date)}`, `${kotlinVer}=${kspVer}` ]; + }) + .flat(1); } (async function main() { const releases = await fetchKspReleases(); - await updateAnchoredMapInFile('../settings.gradle.kts', { - anchorTag: 'KSP_VERSION_MAP', - mapName: 'kspVersionMap', - lines: parseReleases(releases), - updatedLabel: 'KSP releases version map', + const lines = parseReleases(releases); + await updateGradleLinesData('ksp-releases', lines, { + label: 'KSP releases version map', }); })().catch((err) => { console.error('Failed to fetch KSP releases:', err); diff --git a/.utils/scrape-and-inject-latest-gradle-wrapper.mjs b/.utils/scrape-and-inject-latest-gradle-wrapper.mjs index 2a200ed8..cbd6f262 100644 --- a/.utils/scrape-and-inject-latest-gradle-wrapper.mjs +++ b/.utils/scrape-and-inject-latest-gradle-wrapper.mjs @@ -2,7 +2,7 @@ import { compareVersionStrings } from './utils/versioning.mjs'; import { fetchGradleReleases } from './fetch-and-parse-gradle-releases.mjs'; -import { readPropertiesSync, writePropertiesSync } from './utils/properties.mjs'; +import { readPropertiesSync, writePropertiesSyncWithMap } from './utils/properties.mjs'; const KEY = 'distributionUrl'; const URL_PREFIX = 'https://services.gradle.org/distributions'; @@ -67,7 +67,7 @@ async function updateGradleWrapperFileContent({ latestGradleVersion, latestGradl const path = '../gradle/wrapper/' + fileName; const messages = []; const props = readPropertiesSync(path); - let propUrl = props[KEY]; + let propUrl = props.get(KEY); if (!propUrl) { propUrl = latestGradleUrl; messages.push(`Append: ${KEY}=${propUrl}`); @@ -91,7 +91,7 @@ async function updateGradleWrapperFileContent({ latestGradleVersion, latestGradl } if (messages.length > 0) { - writePropertiesSync(path, props); + writePropertiesSyncWithMap(path, props); const maxLength = Math.max(...messages.join('\n').split('\n').map(s => s.length)); const SEP_EQ = '='.repeat(maxLength); const SEP_DASH = '-'.repeat(maxLength); diff --git a/.utils/scrape-and-update-readme-template-contributors-table.mjs b/.utils/scrape-and-update-readme-template-contributors-table.mjs index bcdbd97e..db2c4d1e 100644 --- a/.utils/scrape-and-update-readme-template-contributors-table.mjs +++ b/.utils/scrape-and-update-readme-template-contributors-table.mjs @@ -4,7 +4,7 @@ import * as fs from 'node:fs'; import * as path from 'node:path'; import { fetchStatistics } from './fetch-and-parse-autojs6-merged-pr-commits-statistics.mjs'; import { objectToLines } from './utils/format.mjs'; -import { printDifferences } from './utils/print.mjs'; +import { printLinesDiffs } from './utils/print.mjs'; import { toYYYYMMDD } from './utils/date.mjs'; const updateCommonJsonFile = () => { @@ -22,7 +22,7 @@ const updateCommonJsonFile = () => { if (from !== to) { fs.writeFileSync(commonJsonPath, JSON.stringify(updatedCommon, null, 2), 'utf8'); console.log('[common.json] Updated (contribution statistics date)'); - printDifferences(objectToLines(commonObj), objectToLines(updatedCommon), { regexForKeyMatching: /"\w+"(?=:)/ }); + printLinesDiffs(objectToLines(commonObj), objectToLines(updatedCommon), { regexForKeyMatching: /"\w+"(?=:)/ }); } }; @@ -79,7 +79,7 @@ const markdownToLines = (md) => { fs.writeFileSync(filePath, updated, { encoding: 'utf-8' }); console.log(`[${filename}] Updated (contribution statistics list)`); if (oldMarkdown) { - printDifferences(markdownToLines(oldMarkdown), markdownToLines(newMarkdown)); + printLinesDiffs(markdownToLines(oldMarkdown), markdownToLines(newMarkdown)); } updateCommonJsonFile(); } else { diff --git a/.utils/utils/anchors.mjs b/.utils/utils/anchors.mjs deleted file mode 100644 index 3588fabb..00000000 --- a/.utils/utils/anchors.mjs +++ /dev/null @@ -1,266 +0,0 @@ -// utils/anchors.mjs - -import * as fsp from 'node:fs/promises'; -import * as path from 'node:path'; -import { escapeRegExp } from './format.mjs'; -import { toUpdatedStamp } from './date.mjs'; -import { printDifferences } from './print.mjs'; - -/** - * @param {string} s - * @returns {string} - */ -const normalize = (s) => String(s).replace(/\s+/g, ''); - -/** - * Generate new block content with the given replacement function in the specified Anchor block.
- * zh-CN: 在指定 Anchor 块中, 用给定的替换函数生成新块内容. - * - * @param {string} src - * @param {string} anchorTag - * @param {(block: string) => { newBlock: string, changed: boolean }} replaceBlockFn - * @returns {{ src: string, changed: boolean }} - */ -function replaceInAnchoredBlock(src, anchorTag, replaceBlockFn) { - const beginTag = `// @AnchorBegin ${anchorTag}`; - const endTag = `// @AnchorEnd ${anchorTag}`; - - const beginIdx = src.indexOf(beginTag); - if (beginIdx === -1) throw new Error(`Anchor tag "${anchorTag}" not found in the source code`); - - const endIdx = src.indexOf(endTag, beginIdx + beginTag.length); - if (endIdx === -1) throw new Error(`Anchor tag "${anchorTag}" not found in the source code`); - - const before = src.slice(0, beginIdx); - const block = src.slice(beginIdx, endIdx); - const after = src.slice(endIdx); - - const { newBlock, changed } = replaceBlockFn(block) || {}; - if (!changed || !newBlock) return { src, changed: false }; - - return { src: before + newBlock + after, changed }; -} - -/** - * Replace a map declaration (like mapOf(...)) in an anchor block - * and automatically refresh the @Updated date when changed.
- * zh-CN: 替换锚点块中的某个 map 声明 (如 mapOf(...)), 并在变更时自动刷新 @Updated 日期. - * - * @param {string} src - * @param {Object} options - * @param {string} options.anchorTag - * @param {string} options.mapName - * @param {string[]} options.lines - * @param {number} [options.linesIndent=4] - * @param {(date?: Date) => string} [options.toUpdatedStamp=toUpdatedStamp] - * @returns {{ src: string, changed: boolean }} - */ -function replaceAnchoredMapBlock(src, { - anchorTag, - mapName, - lines, - linesIndent = 4, - toUpdatedStamp: toStamp = toUpdatedStamp, -}) { - return replaceInAnchoredBlock(src, anchorTag, (block) => { - let changed = false; - - const re = new RegExp(String.raw`([\t ]*)(va[lr]\s+)?${escapeRegExp(mapName)}\s*=\s*mapOf\(.*?\)(,?)`, 's'); - - let updatedBlock = block.replace(re, (/** @type {string} */ original, /** @type {string} */ indent, /** @type {string} */ keyword, /** @type {string} */ comma) => { - const kw = keyword ?? ''; - const body = lines.map(l => `${' '.repeat(linesIndent)}${indent}${l}`).join('\n'); - const next = `${indent}${kw}${mapName} = mapOf(\n${body}\n${indent})${comma}`; - if (normalize(original) !== normalize(next)) changed = true; - return next; - }); - if (changed) { - updatedBlock = updatedBlock.replace( - /(@Updated[^\n]*?\son\s)([A-Z][a-z]{2}\s\d{1,2},\s\d{4})(\.?)/, - (_, p1, _old, p3) => `${p1}${(toStamp())}${p3}`, - ); - } - - return { newBlock: updatedBlock, changed }; - }); -} - -/** - * Replace a list declaration (like listOf(...)) in an anchor block - * and automatically refresh the @Updated date when changed.
- * zh-CN:
- * 替换锚点块中的某个 list 声明 (如 listOf(...)), 并在变更时自动刷新 @Updated 日期. - * - * @param {string} src - * @param {Object} options - * @param {string} options.anchorTag - * @param {string} options.listName - * @param {string[]} options.lines - * @param {number} [options.linesIndent=4] - * @param {(date?: Date) => string} [options.toUpdatedStamp=toUpdatedStamp] - * @returns {{ src: string, changed: boolean }} - */ -function replaceAnchoredListBlock(src, { - anchorTag, - listName, - lines, - linesIndent = 4, - toUpdatedStamp: toStamp = toUpdatedStamp, -}) { - return replaceInAnchoredBlock(src, anchorTag, (block) => { - let changed = false; - - const re = new RegExp(`([\\t\\x20]*)(va[lr]\\s+)?${listName}\\s*=\\s*listOf\\([\\s\\S]*?\\)(,?)`, 'm'); - let updatedBlock = block.replace(re, (original, indent, keyword, comma) => { - const kw = keyword ?? ''; - const body = lines.map(l => `${' '.repeat(linesIndent)}${indent}${l}`).join('\n'); - const next = `${indent}${kw}${listName} = listOf(\n${body}\n${indent})${comma}`; - if (normalize(original) !== normalize(next)) changed = true; - return next; - }); - if (changed) { - updatedBlock = updatedBlock.replace( - /(@Updated[^\n]*?\son\s)([A-Z][a-z]{2}\s\d{1,2},\s\d{4})(\.?)/, - (_, p1, _old, p3) => `${p1}${(toStamp())}${p3}`, - ); - } - - return { newBlock: updatedBlock, changed }; - }); -} - -/** - * @param {string} filePath - * @param {Object} options - * @param {string} options.anchorTag - * @param {string} options.mapName - * @param {string[]} options.lines - * @param {number} [options.linesIndent=4] - * @param {string} [options.updatedLabel=''] - * @param {(date?: Date) => string} [options.toUpdatedStamp=toUpdatedStamp] - * @param {RegExp} [options.regexForKeyMatching=null] - * @returns {Promise<{ changed: boolean, content: string }>} - */ -export async function updateAnchoredMapInFile(filePath, { - anchorTag, - mapName, - lines, - linesIndent = 4, - updatedLabel = '', - toUpdatedStamp: toStamp = toUpdatedStamp, - regexForKeyMatching = null, -}) { - const filename = path.basename(filePath); - const raw = await fsp.readFile(filePath, 'utf8'); - const { src: updated, changed } = replaceAnchoredMapBlock(raw, { anchorTag, mapName, lines, linesIndent, toUpdatedStamp: toStamp }); - - if (changed) { - await fsp.writeFile(filePath, updated, 'utf8'); - console.log(`[${filename}] Updated` + (updatedLabel ? ` (${updatedLabel})` : '')); - printDifferences(raw, updated, { regexForKeyMatching }); - } else { - // console.log(`[${filename}] No update needed` + (updatedLabel ? ` (${updatedLabel})` : '')); - } - return { changed, content: updated }; -} - -/** - * @param {string} filePath - * @param {Object} options - * @param {string} options.anchorTag - * @param {string} options.listName - * @param {string[]} options.lines - * @param {number} [options.linesIndent=4] - * @param {string} [options.updatedLabel=''] - * @param {(date?: Date) => string} [options.toUpdatedStamp=toUpdatedStamp] - * @param {RegExp} [options.regexForKeyMatching=null] - * @returns {Promise<{ changed: boolean, content: string }>} - */ -export async function updateAnchoredListInFile(filePath, { - anchorTag, - listName, - lines, - linesIndent = 4, - updatedLabel = '', - toUpdatedStamp: toStamp = toUpdatedStamp, - regexForKeyMatching = null, -}) { - const filename = path.basename(filePath); - const raw = await fsp.readFile(filePath, 'utf8'); - const { src: updated, changed } = replaceAnchoredListBlock(raw, { anchorTag, listName, lines, linesIndent, toUpdatedStamp: toStamp }); - - if (changed) { - await fsp.writeFile(filePath, updated, 'utf8'); - console.log(`[${filename}] Updated` + (updatedLabel ? ` (${updatedLabel})` : '')); - printDifferences(raw, updated, { regexForKeyMatching }); - } else { - // console.log(`[${filename}] No update needed` + (updatedLabel ? ` (${updatedLabel})` : '')); - } - return { changed, content: updated }; -} - -/** - * Batch replace multiple anchors within the same file - * (supports both map and list, requiring only one read/write operation).
- * zh-CN: 批量在同一文件内进行多锚点替换 (同时支持 map 与 list, 读写仅需一次). - * - * @param {string} filePath - * @param {AnchoredBlockUpdateOption[]} optionList - * @param {Object} [extraOptions={}] - * @param {(date?: Date) => string} [extraOptions.toUpdatedStamp=toUpdatedStamp] - * @param {RegExp} [extraOptions.regexForKeyMatching=null] - * @returns {Promise<{ changed: boolean, content: string }>} - */ -export async function batchUpdateAnchoredBlocks(filePath, optionList, { - toUpdatedStamp: toStamp = toUpdatedStamp, - regexForKeyMatching = null, -} = {}) { - const filename = path.basename(filePath); - - let raw = await fsp.readFile(filePath, 'utf8'); - let changedAny = false; - let updated = null; - let updatedLabel = null; - - for (const opt of optionList) { - let res = { src: raw, changed: false }; - - if (opt.type === 'map') { - res = replaceAnchoredMapBlock(raw, { - anchorTag: opt.anchorTag, - mapName: opt.mapName, - lines: opt.lines, - linesIndent: opt.linesIndent, - toUpdatedStamp: toStamp, - }); - } else if (opt.type === 'list') { - res = replaceAnchoredListBlock(raw, { - anchorTag: opt.anchorTag, - listName: opt.listName, - lines: opt.lines, - linesIndent: opt.linesIndent, - toUpdatedStamp: toStamp, - }); - } else if (opt.type === 'custom' && typeof opt.replacer === 'function') { - res = replaceInAnchoredBlock(raw, opt.anchorTag, (block) => opt.replacer(block, { toUpdatedStamp: toStamp })); - } else { - console.warn(`[${filename}] Unknown operation type or missing parameters:`, opt); - continue; - } - - if (res.changed) { - changedAny = true; - updated = res.src; - updatedLabel = opt.updatedLabel; - } - } - - if (changedAny) { - await fsp.writeFile(filePath, updated, 'utf8'); - console.log(`[${filename}] Updated` + (updatedLabel ? ` (${updatedLabel})` : '')); - printDifferences(raw, updated, { regexForKeyMatching }); - } else { - // console.log(`[${filename}] No update needed (${op['updatedLabel'] ?? op.anchorTag})`); - } - return { changed: changedAny, content: changedAny ? updated : raw }; -} diff --git a/.utils/utils/print.mjs b/.utils/utils/print.mjs index d28b21ad..2961e540 100644 --- a/.utils/utils/print.mjs +++ b/.utils/utils/print.mjs @@ -24,7 +24,7 @@ const findCommonSet = (a1, a2) => { * @param {RegExp} [options.regexForKeyMatching] * @return {void} */ -export function printDifferences(src, other, options = {}) { +export function printLinesDiffs(src, other, options = {}) { const leftRaw = extractConcernedLines(src); const rightRaw = extractConcernedLines(other); const commonSet = findCommonSet(leftRaw, rightRaw); @@ -134,3 +134,27 @@ export function printDifferences(src, other, options = {}) { } }); } + +/** + * @param {Map} src + * @param {Map} other + */ +export function printMapDiffs(src, other) { + printLinesDiffs(mapToLines(src), mapToLines(other)); +} + +/** + * @param {string[]} src + * @param {string[]} other + */ +export function printListDiffs(src, other) { + printLinesDiffs(src.join('\n'), other.join('\n')); +} + +/** + * @param {Map} map + * @returns {string} + */ +function mapToLines(map) { + return Array.from(map.entries()).map(([ k, v ]) => `"${k}" to "${v}"`).join('\n'); +} \ No newline at end of file diff --git a/.utils/utils/properties.mjs b/.utils/utils/properties.mjs index bfc6ea62..2d404ffd 100644 --- a/.utils/utils/properties.mjs +++ b/.utils/utils/properties.mjs @@ -4,6 +4,7 @@ import * as fs from 'node:fs'; import * as fsp from 'node:fs/promises'; import { compareVersionStrings } from './versioning.mjs'; import { generatePropertiesFileTimestamp } from './date.mjs'; +import { sortByMap } from './sorting.mjs'; /** * Convert JS string to .properties format escaping rules (store format) @@ -159,10 +160,11 @@ function unescapeProperty(str) { /** * @param {string} text - * @returns {Object} + * @param {MapSortable['sort']} [sortingPattern=null] + * @returns {Map} */ -function parseProperties(text) { - const props = Object.create(null); +export function parseProperties(text, sortingPattern = null) { + const props = new Map(); if (!text) return props; const lines = []; @@ -243,75 +245,115 @@ function parseProperties(text) { const k = unescapeProperty(key); const v = unescapeProperty(value.trim()); - if (k) props[k] = v; + if (k) props.set(k, v); } - return props; + return new Map(sortByMap(props.entries(), sortingPattern)); } /** * @param {string} [filePath='../version.properties'] - * @param {Object} options - * @param {BufferEncoding} [options.encoding='utf8'] - * @returns {Promise>} + * @param {GradleMapRwOptions} [options={}] + * @returns {Promise>} */ -export async function readProperties(filePath = '../version.properties', { encoding = 'utf8' } = {}) { +export async function readProperties(filePath = '../version.properties', { encoding = 'utf8', sort = null } = {}) { const text = await fsp.readFile(filePath, { encoding }); - return parseProperties(text); + return parseProperties(text, sort); } /** * @param {string} [filePath='../version.properties'] - * @param {Object} options - * @param {BufferEncoding} [options.encoding='utf8'] - * @returns {Object} + * @param {GradleMapRwOptions} [options={}] + * @returns {Map} */ -export function readPropertiesSync(filePath = '../version.properties', { encoding = 'utf8' } = {}) { +export function readPropertiesSync(filePath = '../version.properties', { encoding = 'utf8', sort = null } = {}) { const text = fs.readFileSync(filePath, { encoding }); - return parseProperties(text); + return parseProperties(text, sort); } /** - * @param {string} [filePath='../version.properties']] - * @param {Object} [props={}] - * @param {Object} options - * @param {BufferEncoding} [options.encoding='utf8'] + * @param {string} [filePath='../version.properties'] + * @param {Map} [map={}] + * @param {GradleMapRwOptions} [options={}] * @returns {Promise} */ -export async function writeProperties(filePath = '../version.properties', props = {}, { encoding = 'utf8' } = {}) { +export async function writePropertiesWithMap(filePath = '../version.properties', map = new Map(), { encoding = 'utf8', sort = null } = {}) { const lines = []; - for (const key in props) { - const value = props[key]; - if (value == null) continue; + sortByMap(map.entries(), sort).forEach(([ key, value ]) => { + if (value == null) return; const k = escapeProperty(String(key), true); const v = escapeProperty(String(value), false); lines.push(`${k}=${v}`); - } + }); lines.unshift(generatePropertiesFileTimestamp()); const text = lines.join('\n') + '\n'; return fsp.writeFile(filePath, text, { encoding }); } /** - * @param {string} [filePath='../version.properties']] - * @param {Object} [props={}] - * @param {Object} options - * @param {BufferEncoding} [options.encoding='utf8'] + * @param {string} [filePath='../version.properties'] + * @param {Map} [map={}] + * @param {GradleMapRwOptions} [options={}] * @returns {void} */ -export function writePropertiesSync(filePath = '../version.properties', props = {}, { encoding = 'utf8' } = {}) { +export function writePropertiesSyncWithMap(filePath = '../version.properties', map = new Map(), { encoding = 'utf8', sort = null } = {}) { const lines = []; - for (const key in props) { - const value = props[key]; - if (value == null) continue; + sortByMap(map.entries(), sort).forEach(([ key, value ]) => { + if (value == null) return; 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[]} [lines=[]] + * @param {GradleDataRwOptions} [options={}] + * @returns {Promise} + */ +export async function writePropertiesWithLines(filePath = '../version.properties', lines = [], { encoding = 'utf8' } = {}) { + const results = []; + lines.forEach((line) => { + if (line.startsWith('#') || line.startsWith('!')) { + results.push(line); + } + const [ key, value ] = parseProperties(line).entries().next().value || []; + if (value == null) return; + const k = escapeProperty(String(key), true); + const v = escapeProperty(String(value), false); + results.push(`${k}=${v}`); + }); + results.unshift(generatePropertiesFileTimestamp()); + const text = results.join('\n') + '\n'; + return fsp.writeFile(filePath, text, { encoding }); +} + +/** + * @param {string} [filePath='../version.properties'] + * @param {string[]} [lines=[]] + * @param {GradleDataRwOptions} [options={}] + * @returns {void} + */ +export function writePropertiesSyncWithLines(filePath = '../version.properties', lines = [], { encoding = 'utf8' } = {}) { + const results = []; + lines.forEach((line) => { + if (line.startsWith('#') || line.startsWith('!')) { + results.push(line); + } + const [ key, value ] = parseProperties(line).entries().next().value || []; + if (value == null) return; + const k = escapeProperty(String(key), true); + const v = escapeProperty(String(value), false); + results.push(`${k}=${v}`); + }); + results.unshift(generatePropertiesFileTimestamp()); + const text = results.join('\n') + '\n'; + return fs.writeFileSync(filePath, text, { encoding }); +} + /** * @param {string} [filePath='../version.properties'] * @param {Object} options @@ -320,7 +362,7 @@ export function writePropertiesSync(filePath = '../version.properties', props = */ export function getMinSupportedAgpVersion(filePath = '../version.properties', { encoding = 'utf8' } = {}) { let minSupportedVersion = null; - Object.entries(readPropertiesSync(filePath, { encoding })).forEach(([ key, value ]) => { + Array.from(readPropertiesSync(filePath, { encoding }).entries()).forEach(([ key, value ]) => { if (!/agp.version.*min.supported|min.supported.*agp.version/i.test(key)) return; if (minSupportedVersion === null || compareVersionStrings(value, minSupportedVersion) < 0) { minSupportedVersion = value; @@ -340,7 +382,7 @@ export function getMinSupportedAgpVersion(filePath = '../version.properties', { */ export function getMinSupportedGradleVersion(filePath = '../version.properties', { encoding = 'utf8' } = {}) { let minSupportedVersion = null; - Object.entries(readPropertiesSync(filePath, { encoding })).forEach(([ key, value ]) => { + Array.from(readPropertiesSync(filePath, { encoding }).entries()).forEach(([ key, value ]) => { if (!/gradle.version.*min.supported|min.supported.*gradle.version/i.test(key)) return; if (minSupportedVersion === null || compareVersionStrings(value, minSupportedVersion) < 0) { minSupportedVersion = value; @@ -373,7 +415,7 @@ export function getJavaVersionInfo(filePath = '../version.properties', { encodin let minSupportedVer = Infinity; let maxSupportedVer = -Infinity; - Object.entries(readPropertiesSync(filePath, { encoding })).forEach(([ key, value ]) => { + Array.from(readPropertiesSync(filePath, { encoding }).entries()).forEach(([ key, value ]) => { const currentVer = parseInt(value, 10); if (/java.version.*min.suggested|min.suggested.*java.version/i.test(key)) { minSuggestedVer = Math.min(minSuggestedVer, currentVer); diff --git a/.utils/utils/sorting.mjs b/.utils/utils/sorting.mjs new file mode 100644 index 00000000..dfafa200 --- /dev/null +++ b/.utils/utils/sorting.mjs @@ -0,0 +1,75 @@ +// utils/sorting.mjs + +import { compareVersionStrings } from './versioning.mjs'; + +/** + * @param {[string, string][] | MapIterator<[string, string]> | Map} src + * @param {MapSortable['sort'] | null} [pattern=null] + * @returns {[string, string][]} + */ +export function sortByMap(src, pattern = null) { + const entries = src instanceof Map ? Array.from(src.entries()) : Array.from(src); + switch (pattern) { + case null: + return entries; + case 'key.ascending': + case 'key.ascending.as.string': + return entries.sort((a, b) => a[0].localeCompare(b[0])); + case 'key.ascending.as.number': + return entries.sort((a, b) => Number(a[0]) - Number(b[0])); + case 'key.ascending.as.version': + return entries.sort((a, b) => compareVersionStrings(a[0], b[0])); + case 'key.descending': + case 'key.descending.as.string': + return entries.sort((a, b) => b[0].localeCompare(a[0])); + case 'key.descending.as.number': + return entries.sort((a, b) => Number(b[0]) - Number(a[0])); + case 'key.descending.as.version': + return entries.sort((a, b) => compareVersionStrings(b[0], a[0])); + case 'value.ascending': + case 'value.ascending.as.string': + return entries.sort((a, b) => a[1].localeCompare(b[1])); + case 'value.ascending.as.number': + return entries.sort((a, b) => Number(a[1]) - Number(b[1])); + case 'value.ascending.as.version': + return entries.sort((a, b) => compareVersionStrings(a[1], b[1])); + case 'value.descending': + case 'value.descending.as.string': + return entries.sort((a, b) => b[1].localeCompare(a[1])); + case 'value.descending.as.number': + return entries.sort((a, b) => Number(b[1]) - Number(a[1])); + case 'value.descending.as.version': + return entries.sort((a, b) => compareVersionStrings(b[1], a[1])); + default: + throw new Error(`Unknown sorting pattern: ${pattern}`); + } +} + +/** + * @param {string[] | SetIterator | Set} src + * @param {ListSortable['sort'] | null} [pattern=null] + * @returns {string[]} + */ +export function sortByList(src, pattern = null) { + const values = src instanceof Set ? Array.from(src.values()) : Array.from(src); + switch (pattern) { + case null: + return values; + case 'ascending': + case 'ascending.as.string': + return values.sort((a, b) => a.localeCompare(b)); + case 'ascending.as.number': + return values.sort((a, b) => Number(a) - Number(b)); + case 'ascending.as.version': + return values.sort((a, b) => compareVersionStrings(a, b)); + case 'descending': + case 'descending.as.string': + return values.sort((a, b) => b.localeCompare(a)); + case 'descending.as.number': + return values.sort((a, b) => Number(b) - Number(a)); + case 'descending.as.version': + return values.sort((a, b) => compareVersionStrings(b, a)); + default: + throw new Error(`Unknown sorting pattern: ${pattern}`); + } +} diff --git a/.utils/utils/update-helper.mjs b/.utils/utils/update-helper.mjs new file mode 100644 index 00000000..2e155410 --- /dev/null +++ b/.utils/utils/update-helper.mjs @@ -0,0 +1,125 @@ +// utils/update-helper.mjs + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { parseProperties, readPropertiesSync, writePropertiesSyncWithLines, writePropertiesSyncWithMap } from './properties.mjs'; +import { printListDiffs, printMapDiffs } from './print.mjs'; +import { sortByList, sortByMap } from './sorting.mjs'; +import { generatePropertiesFileTimestamp } from './date.mjs'; + +/** + * @param {string} filename + * @param {Map} map + * @param {GradleMapUpdateOptions} [options={}] + * @returns {Promise} + */ +export async function updateGradleMapData(filename, map, options = {}) { + const niceName = filename.endsWith('.properties') ? filename : `${filename}.properties`; + const filePath = path.resolve(process.cwd(), `../gradle/data/${niceName}`); + if (!fs.existsSync(filePath)) { + throw new Error(`File not found: ${filePath}`); + } + const rwOptions = { + encoding: options.encoding || 'utf8', + sort: options.sort, + }; + const original = readPropertiesSync(filePath, rwOptions); + const updated = new Map(sortByMap(map.entries(), options.sort)); + if (!shallowEqualMaps(original, updated)) { + writePropertiesSyncWithMap(filePath, updated, rwOptions); + console.log(`[${niceName}] Updated` + (options.label ? ` (${options.label})` : '')); + printMapDiffs(original, updated); + } else { + // console.log(`[${filename}] No update needed` + (options.label ? ` (${options.label})` : '')); + } +} + +/** + * @param {string} filename + * @param {Set} list + * @param {GradleListUpdateOptions} [options={}] + * @returns {Promise} + */ +export async function updateGradleListData(filename, list, options = {}) { + const niceName = filename.endsWith('.list') ? filename : `${filename}.list`; + const filePath = path.resolve(process.cwd(), `../gradle/data/${niceName}`); + if (!fs.existsSync(filePath)) { + throw new Error(`File not found: ${filePath}`); + } + const rwOptions = { + encoding: options.encoding || 'utf8', + sort: options.sort, + }; + const original = fs.readFileSync(filePath, { encoding: rwOptions.encoding }) + .split('\n') + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + const niceList = Array.from(list) + .map(line => line.trim()) + .filter(line => line && !line.startsWith('#')); + const updated = sortByList(niceList, options.sort); + if (!shallowEqualLists(original, updated)) { + fs.writeFileSync(filePath, generatePropertiesFileTimestamp() + '\n' + updated.join('\n') + '\n', { encoding: rwOptions.encoding }); + console.log(`[${niceName}] Updated` + (options.label ? ` (${options.label})` : '')); + printListDiffs(original, updated); + } else { + // console.log(`[${filename}] No update needed` + (options.label ? ` (${options.label})` : '')); + } +} + +/** + * @param {string} filename + * @param {string[]} lines + * @param {GradleLinesUpdateOptions} [options={}] + * @returns {Promise} + */ +export async function updateGradleLinesData(filename, lines, options = {}) { + const niceName = filename.endsWith('.properties') ? filename : `${filename}.properties`; + const filePath = path.resolve(process.cwd(), `../gradle/data/${niceName}`); + if (!fs.existsSync(filePath)) { + throw new Error(`File not found: ${filePath}`); + } + const rwOptions = { + encoding: options.encoding || 'utf8', + }; + const original = readPropertiesSync(filePath, rwOptions); + const linesToCheck = parseProperties(lines.join('\n')); + if (!shallowEqualMaps(original, linesToCheck)) { + writePropertiesSyncWithLines(filePath, lines, rwOptions); + console.log(`[${niceName}] Updated` + (options.label ? ` (${options.label})` : '')); + printMapDiffs(original, linesToCheck); + } else { + // console.log(`[${filename}] No update needed` + (options.label ? ` (${options.label})` : '')); + } +} + +/** + * @param {Map} a + * @param {Map} b + * @returns {boolean} + */ +function shallowEqualMaps(a, b) { + if (a === b) return true; + if (!(a instanceof Map) || !(b instanceof Map)) return false; + if (a.size !== b.size) return false; + for (const [ k, v ] of a) { + if (!b.has(k)) return false; + if (b.get(k) !== v) return false; + } + return true; +} + +/** + * @param {string[]} a + * @param {string[]} b + * @returns {boolean} + */ +function shallowEqualLists(a, b) { + if (a === b) return true; + if (!Array.isArray(a) || !Array.isArray(b)) return false; + if (a.length !== b.length) return false; + for (let i = 0; i < a.length; i++) { + if (a[i] !== b[i]) return false; + } + return true; +} diff --git a/app/src/main/java/org/autojs/autojs/runtime/api/augment/app/App.kt b/app/src/main/java/org/autojs/autojs/runtime/api/augment/app/App.kt index 76b77905..eb8f2043 100644 --- a/app/src/main/java/org/autojs/autojs/runtime/api/augment/app/App.kt +++ b/app/src/main/java/org/autojs/autojs/runtime/api/augment/app/App.kt @@ -6,7 +6,6 @@ import android.os.Handler import android.os.Looper import android.provider.Settings import androidx.core.net.toUri -import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.schedulers.Schedulers import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -30,7 +29,6 @@ import org.autojs.autojs.runtime.api.augment.Augmentable import org.autojs.autojs.runtime.api.augment.shell.Shell import org.autojs.autojs.runtime.exception.ShouldNeverHappenException import org.autojs.autojs.runtime.exception.WrappedIllegalArgumentException -import org.autojs.autojs.timing.IntentTask import org.autojs.autojs.timing.TimedTaskManager import org.autojs.autojs.util.RhinoUtils import org.autojs.autojs.util.RhinoUtils.UNDEFINED diff --git a/gradle/data/agp-gradle-compat.properties b/gradle/data/agp-gradle-compat.properties new file mode 100644 index 00000000..91db93db --- /dev/null +++ b/gradle/data/agp-gradle-compat.properties @@ -0,0 +1,13 @@ +#Wed Oct 01 21:45:47 GMT+8 2025 +8.13=8.13 +8.12=8.13 +8.11=8.13 +8.10=8.11.1 +8.9=8.11.1 +8.8=8.10.2 +8.7=8.9 +8.6=8.7 +8.5=8.7 +8.4=8.6 +8.3=8.4 +8.2=8.2 diff --git a/gradle/data/agp-releases.list b/gradle/data/agp-releases.list new file mode 100644 index 00000000..b63eb342 --- /dev/null +++ b/gradle/data/agp-releases.list @@ -0,0 +1,14 @@ +#Thu Oct 02 00:55:28 GMT+8 2025 +9.0.0-alpha08 +8.13.0 +8.12.3 +8.11.2 +8.10.1 +8.9.3 +8.8.2 +8.7.3 +8.6.1 +8.5.2 +8.4.2 +8.3.2 +8.2.2 diff --git a/gradle/data/android-studio-agp-compat.properties b/gradle/data/android-studio-agp-compat.properties new file mode 100644 index 00000000..e883c0f1 --- /dev/null +++ b/gradle/data/android-studio-agp-compat.properties @@ -0,0 +1,11 @@ +#Wed Oct 01 22:39:39 GMT+8 2025 +2025.1.3=8.13 +2025.1.2=8.12 +2025.1.1=8.11 +2024.3.2=8.10 +2024.3.1=8.9 +2024.2.2=8.8 +2024.2.1=8.7 +2024.1.2=8.6 +2024.1.1=8.5 +2023.3.1=8.4 diff --git a/gradle/data/android-studio-build-version.properties b/gradle/data/android-studio-build-version.properties new file mode 100644 index 00000000..931ae287 --- /dev/null +++ b/gradle/data/android-studio-build-version.properties @@ -0,0 +1,154 @@ +#Wed Oct 01 18:32:59 GMT+8 2025 +252.25557.131.2521.14170811=2025.2.1.3 +252.25557.131.2521.14165931=2025.2.1.2 +252.25557.131.2521.14126747=2025.2.1.1 +251.27812.49.2514.14171003=2025.1.4.7 +251.27812.49.2514.14156358=2025.1.4.6 +251.27812.49.2514.14085407=2025.1.4.5 +251.27812.49.2514.14044074=2025.1.4.4 +251.27812.49.2514.14014849=2025.1.4.3 +251.27812.49.2514.13971338=2025.1.4.2 +251.26094.121.2514.13932717=2025.1.4.1 +251.26094.121.2513.14007798=2025.1.3.7 +251.26094.121.2513.13991806=2025.1.3.6 +251.26094.121.2513.13930206=2025.1.3.5 +251.26094.121.2513.13898001=2025.1.3.4 +251.26094.121.2513.13865387=2025.1.3.3 +251.26094.121.2513.13829813=2025.1.3.2 +251.26094.121.2513.13793675=2025.1.3.1 +251.26094.121.2512.13991807=2025.1.2.13 +251.26094.121.2512.13930704=2025.1.2.12 +251.26094.121.2512.13840223=2025.1.2.11 +251.26094.121.2512.13795096=2025.1.2.10 +251.26094.121.2512.13760328=2025.1.2.9 +251.26094.121.2512.13728066=2025.1.2.8 +251.26094.121.2512.13699665=2025.1.2.7 +251.26094.121.2512.13669762=2025.1.2.6 +251.26094.121.2512.13636022=2025.1.2.5 +251.26094.121.2512.13602698=2025.1.2.4 +251.25410.109.2512.13571611=2025.1.2.3 +251.25410.109.2512.13536108=2025.1.2.2 +251.25410.109.2512.13514151=2025.1.2.1 +251.25410.109.2511.13752376=2025.1.1.14 +251.25410.109.2511.13665796=2025.1.1.13 +251.25410.109.2511.13625888=2025.1.1.12 +251.25410.109.2511.13569669=2025.1.1.11 +251.23774.435.2511.13464725=2025.1.1.10 +251.23774.435.2511.13434847=2025.1.1.9 +251.23774.16.2511.13409254=2025.1.1.8 +251.23774.16.2511.13371696=2025.1.1.7 +251.23774.16.2511.13338727=2025.1.1.6 +251.23774.16.2511.13328238=2025.1.1.5 +251.23774.16.2511.13307899=2025.1.1.4 +251.23774.16.2511.13279860=2025.1.1.3 +251.23774.16.2511.13244498=2025.1.1.2 +251.23774.16.2511.13239840=2025.1.1.1 +243.26053.27.2432.13536105=2024.3.2.15 +243.25659.59.2432.13423653=2024.3.2.14 +243.25659.59.2432.13404313=2024.3.2.13 +243.25659.59.2432.13371692=2024.3.2.12 +243.25659.59.2432.13314846=2024.3.2.11 +243.25659.59.2432.13282907=2024.3.2.10 +243.25659.59.2432.13218502=2024.3.2.9 +243.24978.46.2432.13174079=2024.3.2.8 +243.24978.46.2432.13133389=2024.3.2.7 +243.24978.46.2432.13093109=2024.3.2.6 +243.23654.117.2432.13058383=2024.3.2.5 +243.23654.117.2432.13021186=2024.3.2.4 +243.23654.117.2432.12988063=2024.3.2.3 +243.22562.218.2432.12959061=2024.3.2.2 +243.22562.218.2432.12933754=2024.3.2.1 +243.24978.46.2431.13363775=2024.3.1.15 +243.24978.46.2431.13208083=2024.3.1.14 +243.22562.218.2431.13114758=2024.3.1.13 +243.22562.218.2431.13054891=2024.3.1.12 +243.22562.218.2431.12996373=2024.3.1.11 +243.22562.218.2431.12926017=2024.3.1.10 +243.22562.218.2431.12894773=2024.3.1.9 +243.22562.145.2431.12862218=2024.3.1.8 +243.22562.145.2431.12832016=2024.3.1.7 +243.22562.145.2431.12789491=2024.3.1.6 +243.21565.193.2431.12752199=2024.3.1.5 +243.21565.193.2431.12735582=2024.3.1.4 +243.21565.193.2431.12691553=2024.3.1.3 +243.21565.129.2431.12655144=2024.3.1.2 +243.21565.23.2431.12637205=2024.3.1.1 +242.23726.103.2422.13103373=2024.2.2.15 +242.23726.103.2422.13016713=2024.2.2.14 +242.23726.103.2422.12816248=2024.2.2.13 +242.23726.103.2422.12785815=2024.2.2.12 +242.23726.103.2422.12721465=2024.2.2.11 +242.23726.103.2422.12630895=2024.2.2.10 +242.23339.11.2422.12584204=2024.2.2.9 +242.23339.11.2422.12548333=2024.2.2.8 +242.23339.11.2422.12512484=2024.2.2.7 +242.23339.11.2422.12480590=2024.2.2.6 +242.23339.11.2422.12448552=2024.2.2.5 +242.23339.11.2422.12417746=2024.2.2.4 +242.21829.142.2422.12395834=2024.2.2.3 +242.21829.142.2422.12358220=2024.2.2.2 +242.21829.142.2422.12329062=2024.2.2.1 +242.23339.11.2421.12700392=2024.2.1.12 +242.23339.11.2421.12550806=2024.2.1.11 +242.23339.11.2421.12483815=2024.2.1.10 +242.21829.142.2421.12409432=2024.2.1.9 +242.21829.142.2421.12366423=2024.2.1.8 +242.21829.142.2421.12332550=2024.2.1.7 +242.20224.300.2421.12312915=2024.2.1.6 +242.20224.300.2421.12293307=2024.2.1.5 +242.20224.300.2421.12279409=2024.2.1.4 +242.20224.300.2421.12232258=2024.2.1.3 +242.20224.300.2421.12216002=2024.2.1.2 +242.20224.300.2421.12201251=2024.2.1.1 +242.20224.159.2421.12169541=2024.2.1.1 +241.18034.62.2413.12138059=2024.1.3.3 +241.18034.62.2413.12109192=2024.1.3.2 +241.18034.62.2413.12081498=2024.1.3.1 +241.19072.14.2412.12360217=2024.1.2.13 +241.18034.62.2412.12266719=2024.1.2.12 +241.18034.62.2412.12169539=2024.1.2.11 +241.18034.62.2412.12084018=2024.1.2.10 +241.18034.62.2412.12048879=2024.1.2.9 +241.18034.62.2412.12022747=2024.1.2.8 +241.17890.1.2412.11993144=2024.1.2.7 +241.17890.1.2412.11972402=2024.1.2.6 +241.17011.79.2412.11938461=2024.1.2.5 +241.15989.150.2412.11905440=2024.1.2.4 +241.15989.150.2412.11879573=2024.1.2.3 +241.15989.150.2412.11851376=2024.1.2.2 +241.15989.150.2412.11822507=2024.1.2.1 +241.18034.62.2411.12169540=2024.1.1.13 +241.18034.62.2411.12071903=2024.1.1.12 +241.15989.150.2411.11948838=2024.1.1.11 +241.15989.150.2411.11932844=2024.1.1.10 +241.15989.150.2411.11887322=2024.1.1.9 +241.15989.150.2411.11840554=2024.1.1.8 +241.15989.150.2411.11801021=2024.1.1.7 +241.15989.150.2411.11792637=2024.1.1.6 +241.14494.240.2411.11771556=2024.1.1.5 +241.14494.240.2411.11731683=2024.1.1.4 +241.14494.240.2411.11700778=2024.1.1.3 +241.14494.158.2411.11678081=2024.1.1.2 +241.14494.158.2411.11648550=2024.1.1.1 +233.14475.28.2332.11606850=2023.3.2.2 +233.14475.28.2332.11578541=2023.3.2.1 +233.14808.21.2331.11926650=2023.3.1.20 +233.14808.21.2331.11842104=2023.3.1.19 +233.14808.21.2331.11709847=2023.3.1.18 +233.14808.21.2331.11678080=2023.3.1.17 +233.14808.21.2331.11643467=2023.3.1.16 +233.14808.21.2331.11608968=2023.3.1.15 +233.14808.21.2331.11574862=2023.3.1.14 +233.14475.28.2331.11543046=2023.3.1.13 +233.14475.28.2331.11514062=2023.3.1.12 +233.14475.28.2331.11483111=2023.3.1.11 +233.14475.28.2331.11456607=2023.3.1.10 +233.14015.106.2331.11423272=2023.3.1.9 +233.14015.106.2331.11392117=2023.3.1.8 +233.13135.103.2331.11360849=2023.3.1.7 +233.13135.103.2331.11350331=2023.3.1.6 +233.13135.103.2331.11329247=2023.3.1.5 +233.13135.103.2331.11301475=2023.3.1.4 +233.13135.103.2331.11273936=2023.3.1.3 +233.13135.103.2331.11259770=2023.3.1.2 +233.13135.103.2331.11247450=2023.3.1.1 diff --git a/gradle/data/android-studio-codename-version.properties b/gradle/data/android-studio-codename-version.properties new file mode 100644 index 00000000..725e50d1 --- /dev/null +++ b/gradle/data/android-studio-codename-version.properties @@ -0,0 +1,20 @@ +#Wed Oct 01 18:32:59 GMT+8 2025 +2025.2=O +2025.1=N +2024.3=M +2024.2=L +2024.1.3=L +2024.1.2=K +2024.1.1=K +2023.3.2.2=K +2023.3.2.1=J +2023.3.1=J +2023.2=I +2023.1=H +2022.3=G +2022.2=F +2022.1=E +2021.3=D +2021.2=C +2021.1=B +2020.3=A diff --git a/gradle/data/android-studio-codename.properties b/gradle/data/android-studio-codename.properties new file mode 100644 index 00000000..0cb4af4d --- /dev/null +++ b/gradle/data/android-studio-codename.properties @@ -0,0 +1,31 @@ +#Wed Oct 01 20:46:54 GMT+8 2025 +#Born on Sep 22, 2025 +O=Otter +#Born on Mar 19, 2025 +N=Narwhal +#Born on Nov 12, 2024 +M=Meerkat +#Born on Jul 15, 2024 +L=Ladybug +#Born on Mar 22, 2024 +K=Koala +#Born on Dec 28, 2023 +J=Jellyfish +#Born on Aug 25, 2023 +I=Iguana +#Born on Apr 25, 2023 +H=Hedgehog +#Born on Jan 17, 2023 +G=Giraffe +#Born on Sep 20, 2022 +F=Flamingo +#Born on May 11, 2022 +E=Electric Eel +#Born on Jan 31, 2022 +D=Dolphin +#Born on Oct 13, 2021 +C=Chipmunk +#Born on May 18, 2021 +B=Bumblebee +#Born on Jan 26, 2021 +A=Arctic Fox diff --git a/gradle/data/gradle-kotlin-compat.properties b/gradle/data/gradle-kotlin-compat.properties new file mode 100644 index 00000000..9ba006ec --- /dev/null +++ b/gradle/data/gradle-kotlin-compat.properties @@ -0,0 +1,11 @@ +#Wed Oct 01 21:14:23 GMT+8 2025 +9.0.0=2.2.0 +8.12=2.0.21 +8.11=2.0.20 +8.10=1.9.24 +8.9=1.9.23 +8.7=1.9.22 +8.5=1.9.20 +8.4=1.9.10 +8.3=1.9.0 +8.2=1.8.20 diff --git a/gradle/data/java-gradle-compat.properties b/gradle/data/java-gradle-compat.properties new file mode 100644 index 00000000..b6ce395f --- /dev/null +++ b/gradle/data/java-gradle-compat.properties @@ -0,0 +1,11 @@ +#Wed Oct 01 23:43:14 GMT+8 2025 +26=N/A +25=9.1 +24=8.14 +23=8.10 +22=8.8 +21=8.5 +20=8.3 +19=7.6 +18=7.5 +17=7.3 diff --git a/gradle/data/ksp-releases.properties b/gradle/data/ksp-releases.properties new file mode 100644 index 00000000..12c33119 --- /dev/null +++ b/gradle/data/ksp-releases.properties @@ -0,0 +1,135 @@ +#Wed Oct 01 22:12:30 GMT+8 2025 +#Sep 12, 2025 +2.2.20=2.0.3 +#Sep 4, 2025 +2.2.20-RC2=2.0.2 +#Aug 20, 2025 +2.2.20-RC=2.0.2 +#Aug 1, 2025 +2.2.20-Beta2=2.0.2 +#Jul 11, 2025 +2.2.20-Beta1=2.0.2 +#Aug 15, 2025 +2.2.10=2.0.2 +#Aug 7, 2025 +2.2.10-RC2=2.0.2 +#Jul 25, 2025 +2.2.10-RC=2.0.2 +#Jun 25, 2025 +2.2.0=2.0.2 +#Jun 17, 2025 +2.2.0-RC3=2.0.2 +#Jun 11, 2025 +2.2.0-RC2=2.0.2 +#May 17, 2025 +2.2.0-RC=2.0.1 +#May 2, 2025 +2.2.0-Beta2=2.0.1 +#Apr 17, 2025 +2.2.0-Beta1=2.0.0 +#Jun 10, 2025 +2.1.21=2.0.2 +#May 2, 2025 +2.1.21-RC2=2.0.1 +#Apr 18, 2025 +2.1.21-RC=2.0.0 +#May 1, 2025 +2.1.20=2.0.1 +#Mar 15, 2025 +2.1.20-RC3=1.0.31 +#Mar 7, 2025 +2.1.20-RC2=1.0.31 +#Feb 28, 2025 +2.1.20-RC=1.0.31 +#Feb 15, 2025 +2.1.20-Beta2=1.0.30 +#Dec 21, 2024 +2.1.20-Beta1=1.0.29 +#Feb 28, 2025 +2.1.10=1.0.31 +#Jan 22, 2025 +2.1.10-RC2=1.0.29 +#Jan 10, 2025 +2.1.10-RC=1.0.29 +#Nov 28, 2024 +2.1.0=1.0.29 +#Nov 20, 2024 +2.1.0-RC2=1.0.28 +#Nov 8, 2024 +2.1.0-RC=1.0.27 +#Oct 26, 2024 +2.1.0-Beta2=1.0.26 +#Sep 19, 2024 +2.1.0-Beta1=1.0.25 +#Nov 16, 2024 +2.0.21=1.0.28 +#Oct 2, 2024 +2.0.21-RC=1.0.25 +#Sep 6, 2024 +2.0.20=1.0.25 +#Aug 14, 2024 +2.0.20-RC2=1.0.24 +#Jul 31, 2024 +2.0.20-RC=1.0.24 +#Jul 12, 2024 +2.0.20-Beta2=1.0.23 +#Jun 21, 2024 +2.0.20-Beta1=1.0.22 +#Aug 7, 2024 +2.0.10=1.0.24 +#Jul 30, 2024 +2.0.10-RC2=1.0.24 +#Jul 12, 2024 +2.0.10-RC=1.0.23 +#Jul 30, 2024 +2.0.0=1.0.24 +#May 14, 2024 +2.0.0-RC3=1.0.20 +#Apr 30, 2024 +2.0.0-RC2=1.0.20 +#Apr 11, 2024 +2.0.0-RC1=1.0.20 +#Apr 5, 2024 +2.0.0-Beta5=1.0.20 +#Mar 8, 2024 +2.0.0-Beta4=1.0.19 +#Jan 20, 2024 +2.0.0-Beta3=1.0.17 +#Dec 20, 2023 +2.0.0-Beta2=1.0.16 +#Nov 28, 2023 +2.0.0-Beta1=1.0.15 +#Jul 24, 2024 +1.9.25=1.0.20 +#May 8, 2024 +1.9.24=1.0.20 +#Apr 5, 2024 +1.9.23=1.0.20 +#Jan 19, 2024 +1.9.22=1.0.17 +#Dec 14, 2023 +1.9.21=1.0.16 +#Nov 3, 2023 +1.9.20=1.0.14 +#Oct 26, 2023 +1.9.20-RC2=1.0.13 +#Oct 12, 2023 +1.9.20-RC=1.0.13 +#Sep 22, 2023 +1.9.20-Beta2=1.0.13 +#Sep 12, 2023 +1.9.20-Beta=1.0.13 +#Aug 24, 2023 +1.9.10=1.0.13 +#Aug 3, 2023 +1.9.0=1.0.13 +#Jun 22, 2023 +1.9.0-RC=1.0.11 +#May 24, 2023 +1.9.0-Beta=1.0.11 +#Jun 9, 2023 +1.8.22=1.0.11 +#Apr 27, 2023 +1.8.21=1.0.11 +#Apr 18, 2023 +1.8.20=1.0.11 diff --git a/settings.gradle.kts b/settings.gradle.kts index 205d185f..c246045d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -47,102 +47,48 @@ modules.forEach { pluginManagement { - // @Hint by SuperMonster003 on May 13, 2025. - // ! Change the null value to specify a version (instead of auto-selection), e.g. 23 or "8.3.0-beta02". - // ! zh-CN: 如需指定版本 (而非自动选择版本), 可修改相应的 null 值, 如 23 或 "8.3.0-beta02". - // ! - // ! Gradle JVM - val overriddenJavaVersion: Int? = null - // ! classpath("com.android.tools.build:gradle") - val overriddenAgpVersion: String? = null - // ! classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") - val overriddenKotlinVersion: String? = null - // ! plugin("com.google.devtools.ksp") - val overriddenKspVersion: String? = null + @Suppress("UNCHECKED_CAST") + fun rootDirProps(name: String) = java.util.Properties().apply { + rootDir.resolve("$name.properties").inputStream().use { load(it) } + } as Map - val versionProperties = java.util.Properties().apply { - rootDir.resolve("version.properties").inputStream().use { load(it) } + fun rootDirList(name: String) = rootDir.resolve("$name.list").inputStream().use { + it.bufferedReader().useLines { lines -> + lines.filter { line -> + line.isNotBlank() && !line.startsWith("#") + }.toList() + } } - // @AnchorBegin GRADLE_KOTLIN_COMPATIBILITY_LIST - // @Script /.utils/scrape-and-inject-gradle-kotlin-compatibility-list.mjs - // @Signature Pair - // @Reference https://docs.gradle.org/current/userguide/compatibility.html#kotlin - // @Updated by SuperMonster003 on Sep 18, 2025. - val gradleKotlinCompatibility = listOf( - "9.0.0" to "2.2.0", - "8.12" to "2.0.21", - "8.11" to "2.0.20", - "8.10" to "1.9.24", - "8.9" to "1.9.23", - "8.7" to "1.9.22", - "8.5" to "1.9.20", - "8.4" to "1.9.10", - "8.3" to "1.9.0", - "8.2" to "1.8.20", - ) - // @AnchorEnd GRADLE_KOTLIN_COMPATIBILITY_LIST + fun gradleDataProps(name: String) = rootDirProps("gradle/data/$name") + fun gradleDataList(name: String) = rootDirList("gradle/data/$name") - // @AnchorBegin JAVA_GRADLE_COMPATIBILITY_LIST - // @Script /.utils/scrape-java-gradle-compatibility-map.mjs - // @Signature Pair - // @Reference https://docs.gradle.org/current/userguide/compatibility.html#java_runtime - // @Updated by SuperMonster003 on Sep 18, 2025. - val javaGradleCompatibility = listOf( - 26 to "N/A", - 25 to "9.1", - 24 to "8.14", - 23 to "8.10", - 22 to "8.8", - 21 to "8.5", - 20 to "8.3", - 19 to "7.6", - 18 to "7.5", - 17 to "7.3", - ) - // @AnchorEnd JAVA_GRADLE_COMPATIBILITY_LIST + val versionProps by lazy { rootDirProps("version") } - // @AnchorBegin AGP_GRADLE_COMPATIBILITY_LIST - // @Script /.utils/scrape-and-inject-agp-gradle-compatibility-list.mjs - // @Signature Pair - // @Reference https://developer.android.com/build/releases/gradle-plugin#updating-gradle - // @Updated by SuperMonster003 on Sep 3, 2025. - val agpGradleCompatibility = listOf( - "8.13" to "8.13", - "8.12" to "8.13", - "8.11" to "8.13", - "8.10" to "8.11.1", - "8.9" to "8.11.1", - "8.8" to "8.10.2", - "8.7" to "8.9", - "8.6" to "8.7", - "8.5" to "8.7", - "8.4" to "8.6", - "8.3" to "8.4", - "8.2" to "8.2", - ) - // @AnchorEnd AGP_GRADLE_COMPATIBILITY_LIST + // @Hint by SuperMonster003 on Oct 2, 2025. + // ! Change the overridden value in version.properties to specify a version (instead of auto-selection), e.g. 23 or "8.9.3". + // ! zh-CN: 如需指定版本 (而非自动选择版本), 可修改 version.properties 相应的 overridden 值, 如 23 或 "8.9.3". + // ! + // ! Gradle JVM [ e.g. 23 ] + val overriddenJavaVersion: Int? = versionProps["OVERRIDDEN_JAVA_VERSION"]?.toIntOrNull() + // ! classpath("com.android.tools.build:gradle") [ e.g. "8.9.3", "8.12.0" ] + val overriddenAgpVersion: String? = versionProps["OVERRIDDEN_ANDROID_GRADLE_PLUGIN_VERSION"]?.takeUnless { it.isBlank() || it == "NONE" } + // ! classpath("org.jetbrains.kotlin:kotlin-gradle-plugin") [ e.g. "1.9.25", "2.0.21" ] + val overriddenKotlinVersion: String? = versionProps["OVERRIDDEN_KOTLIN_GRADLE_PLUGIN_VERSION"]?.takeUnless { it.isBlank() || it == "NONE" } + // ! plugin("com.google.devtools.ksp") [ e.g. "1.9.25-1.0.20", "2.0.21-1.0.28" ] + val overriddenKspVersion: String? = versionProps["OVERRIDDEN_KSP_GRADLE_PLUGIN_VERSION"]?.takeUnless { it.isBlank() || it == "NONE" } - // @AnchorBegin ANDROID_GRADLE_PLUGIN_RELEASES_LIST - // @Script /.utils/scrape-and-inject-agp-releases.mjs - // @Reference https://developer.android.com/reference/tools/gradle-api - // @Updated by SuperMonster003 on Sep 28, 2025. - val agpReleases = listOf( - "9.0.0-alpha08", - "8.13.0", - "8.12.3", - "8.11.2", - "8.10.1", - "8.9.3", - "8.8.2", - "8.7.3", - "8.6.1", - "8.5.2", - "8.4.2", - "8.3.2", - "8.2.2", - ) - // @AnchorEnd ANDROID_GRADLE_PLUGIN_RELEASES_LIST + val agpReleases by lazy { gradleDataList("agp-releases") } + + val gradleKotlinCompatProps by lazy { gradleDataProps("gradle-kotlin-compat") } + val javaGradleCompatProps by lazy { gradleDataProps("java-gradle-compat") } + val agpGradleCompatProps by lazy { gradleDataProps("agp-gradle-compat") } + val kspReleaseProps by lazy { gradleDataProps("ksp-releases") } + + val androidStudioAgpCompatProps by lazy { gradleDataProps("android-studio-agp-compat") } + val androidStudioBuildVersionProps by lazy { gradleDataProps("android-studio-build-version") } + val androidStudioCodenameVersionProps by lazy { gradleDataProps("android-studio-codename-version") } + val androidStudioCodenameProps by lazy { gradleDataProps("android-studio-codename") } val consts = object { val DEFAULT_VERSION = "0" @@ -212,13 +158,13 @@ pluginManagement { val utils = object { - private val SUFFIX_PRIORITY: Map = mapOf( + private val suffixPriorityMap = mapOf( "canary" to 1, "nightly" to 1, "snapshot" to 1, "dev" to 1, "pre-alpha" to 2, "prealpha" to 2, "preview" to 2, "eap" to 2, "milestone" to 2, "alpha" to 3, "beta" to 4, "rc" to 5, - "" to 10, "stable" to 10, "ga" to 10, "final" to 10, "release" to 10, "lts" to 10 + "" to 10, "stable" to 10, "ga" to 10, "final" to 10, "release" to 10, "lts" to 10, ) private fun normalizeSuffixName(raw: String?): String { @@ -256,8 +202,8 @@ pluginManagement { val name1 = normalizeSuffixName(name1Raw) val name2 = normalizeSuffixName(name2Raw) - val p1 = SUFFIX_PRIORITY[name1] ?: Int.MAX_VALUE - val p2 = SUFFIX_PRIORITY[name2] ?: Int.MAX_VALUE + val p1 = suffixPriorityMap[name1] ?: Int.MAX_VALUE + val p2 = suffixPriorityMap[name2] ?: Int.MAX_VALUE val byPriority = p1.compareTo(p2) if (byPriority != 0) return byPriority @@ -293,6 +239,8 @@ pluginManagement { .orElse(providers.systemProperty("android.studio.version")) .orNull ?: return null + androidStudioBuildVersionProps[build]?.let { return it } + val parts = build.split('.') val baseStr = parts.getOrNull(0) ?: return null val base = baseStr.toIntOrNull() ?: return null @@ -313,6 +261,7 @@ pluginManagement { return if (patch != null && patch > 0) "$year.$minor.$patch" else "$year.$minor" } + } val config = object { @@ -326,97 +275,30 @@ pluginManagement { val isCleanupPaddleOcr = false val isCleanupRapidOcr = false - val fallbackAgpVersion = "8.2.2" - val fallbackKotlinVersion = "1.8.10" - @Suppress("unused") val platforms = object { val androidStudio = object : Platform( name = "AndroidStudio", vendor = "Google", - // @AnchorBegin ANDROID_STUDIO_AGP_VERSION_MAP - // @Script /.utils/scrape-android-studio-agp_version_maps.mjs - // @Reference https://developer.android.com/studio/releases#android_gradle_plugin_and_android_studio_compatibility - // @Updated by SuperMonster003 on Sep 5, 2025. - agpVersionMap = mapOf( - "2025.1.3" to "8.13", - "2025.1.2" to "8.12", - "2025.1.1" to "8.11", - "2024.3.2" to "8.10", - "2024.3.1" to "8.9", - "2024.2.2" to "8.8", - "2024.2.1" to "8.7", - "2024.1.2" to "8.6", - "2024.1.1" to "8.5", - "2023.3.1" to "8.4", - ), - // @AnchorEnd ANDROID_STUDIO_AGP_VERSION_MAP - - // @AnchorBegin ANDROID_STUDIO_CODENAME_VERSION_MAP - // @Script /.utils/scrape-and-inject-android-studio-codename_maps.mjs - // @Reference https://developer.android.com/studio/archive?hl=en - // @Updated by SuperMonster003 on Sep 23, 2025. - codenameVersionMap = mapOf( - "2025.2" to "O", - "2025.1" to "N", - "2024.3" to "M", - "2024.2" to "L", - "2024.1.3" to "L", - "2024.1.2" to "K", - "2024.1.1" to "K", - "2023.3.2" to "J|K", - "2023.3.1" to "J", - "2023.2" to "I", - "2023.1" to "H", - "2022.3" to "G", - "2022.2" to "F", - "2022.1" to "E", - "2021.3" to "D", - "2021.2" to "C", - "2021.1" to "B", - "2020.3" to "A", - ), - // @AnchorEnd ANDROID_STUDIO_CODENAME_VERSION_MAP - - // @AnchorBegin ANDROID_STUDIO_CODENAME_MAP - // @Script /.utils/scrape-and-inject-android-studio-codename_maps.mjs - // @Reference https://developer.android.com/studio/archive?hl=en - // @Updated by SuperMonster003 on Sep 23, 2025. - codenameMap = mapOf( - "O" to "Otter", /* Born on Sep 22, 2025. */ - "N" to "Narwhal", /* Born on Mar 19, 2025. */ - "M" to "Meerkat", /* Born on Nov 12, 2024. */ - "L" to "Ladybug", /* Born on Jul 15, 2024. */ - "K" to "Koala", /* Born on Mar 22, 2024. */ - "J" to "Jellyfish", /* Born on Dec 28, 2023. */ - "I" to "Iguana", /* Born on Aug 25, 2023. */ - "H" to "Hedgehog", /* Born on Apr 25, 2023. */ - "G" to "Giraffe", /* Born on Jan 17, 2023. */ - "F" to "Flamingo", /* Born on Sep 20, 2022. */ - "E" to "Electric Eel", /* Born on May 11, 2022. */ - "D" to "Dolphin", /* Born on Jan 31, 2022. */ - "C" to "Chipmunk", /* Born on Oct 13, 2021. */ - "B" to "Bumblebee", /* Born on May 18, 2021. */ - "A" to "Arctic Fox", /* Born on Jan 26, 2021. */ - ), - // @AnchorEnd ANDROID_STUDIO_CODENAME_MAP + agpVersionMap = androidStudioAgpCompatProps, ) { override val weight = Int.MAX_VALUE override val gradleSettingsName = "Gradle JDK" override val fullName by lazy { - val suffix = codenameVersionMap?.let { map -> - val letters = map[version] - ?: map[version.split(".").take(2).joinToString(".")] + val suffix = androidStudioCodenameVersionProps.let { prop -> + val letters = prop[version] + ?: prop[version.split(".").take(3).joinToString(".")] + ?: prop[version.split(".").take(2).joinToString(".")] ?: return@let null letters .split("|") .joinToString(" / ", prefix = " ") { key -> - codenameMap?.get(key.trim()) ?: key + androidStudioCodenameProps[key.trim()] ?: key } } ?: "" return@lazy "Android Studio$suffix" } - override val minSupportedVersion = versionProperties["MIN_SUPPORTED_ANDROID_STUDIO_IDE_VERSION"] as String + override val minSupportedVersion = versionProps["MIN_SUPPORTED_ANDROID_STUDIO_IDE_VERSION"] as String } val intelliJIdea = object : Platform( @@ -436,7 +318,7 @@ pluginManagement { override val weight = 10 override val gradleSettingsName = "Gradle JVM" override val fullName = "IntelliJ IDEA" - override val minSupportedVersion = versionProperties["MIN_SUPPORTED_INTELLIJ_IDEA_IDE_VERSION"] as String + override val minSupportedVersion = versionProps["MIN_SUPPORTED_INTELLIJ_IDEA_IDE_VERSION"] as String } val temurin = object : Platform( @@ -512,87 +394,10 @@ pluginManagement { Plugin(id = "org.gradle.toolchains.foojay-resolver-convention", version = "toml:foojay-resolver-convention"), ) - // @AnchorBegin KSP_VERSION_MAP - // @Script /.utils/scrape-and-inject-ksp-releases.mjs - // @Reference https://github.com/google/ksp/releases - // @Updated by SuperMonster003 on Sep 12, 2025. - val kspVersionMap = mapOf( - "2.2.20" to "2.0.3", /* Sep 12, 2025. */ - "2.2.20-RC2" to "2.0.2", /* Sep 4, 2025. */ - "2.2.20-RC" to "2.0.2", /* Aug 20, 2025. */ - "2.2.20-Beta2" to "2.0.2", /* Aug 1, 2025. */ - "2.2.20-Beta1" to "2.0.2", /* Jul 11, 2025. */ - "2.2.10" to "2.0.2", /* Aug 15, 2025. */ - "2.2.10-RC2" to "2.0.2", /* Aug 7, 2025. */ - "2.2.10-RC" to "2.0.2", /* Jul 25, 2025. */ - "2.2.0" to "2.0.2", /* Jun 25, 2025. */ - "2.2.0-RC3" to "2.0.2", /* Jun 17, 2025. */ - "2.2.0-RC2" to "2.0.2", /* Jun 11, 2025. */ - "2.2.0-RC" to "2.0.1", /* May 17, 2025. */ - "2.2.0-Beta2" to "2.0.1", /* May 2, 2025. */ - "2.2.0-Beta1" to "2.0.0", /* Apr 17, 2025. */ - "2.1.21" to "2.0.2", /* Jun 10, 2025. */ - "2.1.21-RC2" to "2.0.1", /* May 2, 2025. */ - "2.1.21-RC" to "2.0.0", /* Apr 18, 2025. */ - "2.1.20" to "2.0.1", /* May 1, 2025. */ - "2.1.20-RC3" to "1.0.31", /* Mar 15, 2025. */ - "2.1.20-RC2" to "1.0.31", /* Mar 7, 2025. */ - "2.1.20-RC" to "1.0.31", /* Feb 28, 2025. */ - "2.1.20-Beta2" to "1.0.30", /* Feb 15, 2025. */ - "2.1.20-Beta1" to "1.0.29", /* Dec 21, 2024. */ - "2.1.10" to "1.0.31", /* Feb 28, 2025. */ - "2.1.10-RC2" to "1.0.29", /* Jan 22, 2025. */ - "2.1.10-RC" to "1.0.29", /* Jan 10, 2025. */ - "2.1.0" to "1.0.29", /* Nov 28, 2024. */ - "2.1.0-RC2" to "1.0.28", /* Nov 20, 2024. */ - "2.1.0-RC" to "1.0.27", /* Nov 8, 2024. */ - "2.1.0-Beta2" to "1.0.26", /* Oct 26, 2024. */ - "2.1.0-Beta1" to "1.0.25", /* Sep 19, 2024. */ - "2.0.21" to "1.0.28", /* Nov 16, 2024. */ - "2.0.21-RC" to "1.0.25", /* Oct 2, 2024. */ - "2.0.20" to "1.0.25", /* Sep 6, 2024. */ - "2.0.20-RC2" to "1.0.24", /* Aug 14, 2024. */ - "2.0.20-RC" to "1.0.24", /* Jul 31, 2024. */ - "2.0.20-Beta2" to "1.0.23", /* Jul 12, 2024. */ - "2.0.20-Beta1" to "1.0.22", /* Jun 21, 2024. */ - "2.0.10" to "1.0.24", /* Aug 7, 2024. */ - "2.0.10-RC2" to "1.0.24", /* Jul 30, 2024. */ - "2.0.10-RC" to "1.0.23", /* Jul 12, 2024. */ - "2.0.0" to "1.0.24", /* Jul 30, 2024. */ - "2.0.0-RC3" to "1.0.20", /* May 14, 2024. */ - "2.0.0-RC2" to "1.0.20", /* Apr 30, 2024. */ - "2.0.0-RC1" to "1.0.20", /* Apr 11, 2024. */ - "2.0.0-Beta5" to "1.0.20", /* Apr 5, 2024. */ - "2.0.0-Beta4" to "1.0.19", /* Mar 8, 2024. */ - "2.0.0-Beta3" to "1.0.17", /* Jan 20, 2024. */ - "2.0.0-Beta2" to "1.0.16", /* Dec 20, 2023. */ - "2.0.0-Beta1" to "1.0.15", /* Nov 28, 2023. */ - "1.9.25" to "1.0.20", /* Jul 24, 2024. */ - "1.9.24" to "1.0.20", /* May 8, 2024. */ - "1.9.23" to "1.0.20", /* Apr 5, 2024. */ - "1.9.22" to "1.0.17", /* Jan 19, 2024. */ - "1.9.21" to "1.0.16", /* Dec 14, 2023. */ - "1.9.20" to "1.0.14", /* Nov 3, 2023. */ - "1.9.20-RC2" to "1.0.13", /* Oct 26, 2023. */ - "1.9.20-RC" to "1.0.13", /* Oct 12, 2023. */ - "1.9.20-Beta2" to "1.0.13", /* Sep 22, 2023. */ - "1.9.20-Beta" to "1.0.13", /* Sep 12, 2023. */ - "1.9.10" to "1.0.13", /* Aug 24, 2023. */ - "1.9.0" to "1.0.13", /* Aug 3, 2023. */ - "1.9.0-RC" to "1.0.11", /* Jun 22, 2023. */ - "1.9.0-Beta" to "1.0.11", /* May 24, 2023. */ - "1.8.22" to "1.0.11", /* Jun 9, 2023. */ - "1.8.21" to "1.0.11", /* Apr 27, 2023. */ - "1.8.20" to "1.0.11", /* Apr 18, 2023. */ - ) - // @AnchorEnd KSP_VERSION_MAP - abstract inner class Platform( var name: String, val vendor: String, val agpVersionMap: Map = emptyMap(), - val codenameVersionMap: Map? = null, - val codenameMap: Map? = null, ) { open val gradleSettingsName: String? = null @@ -610,7 +415,7 @@ pluginManagement { || systemProperties.vendorName?.contains(vendor, true) == true fun ensureMinimalGradleJdkVersion() { - val minVer = versionProperties["JAVA_VERSION_MIN_SUPPORTED"].let { it as String }.toInt() + val minVer = versionProps["JAVA_VERSION_MIN_SUPPORTED"].let { it as String }.toInt() if (JavaVersion.current().majorVersion.toInt() < minVer) { Formatted( @@ -648,29 +453,19 @@ pluginManagement { } val identifier = object { - - // @Hint by SuperMonster003 on Oct 18, 2024. - // ! Fallback is a temporary or alternative solution - // ! used to handle some specific situations where functionality is unavailable. - // ! Compatibility is a broader design principle which ensures new versions - // ! remain compatible and continue to support the operations and data of older versions. - // ! zh-CN: - // ! Fallback (回退) 是一种临时或替代的解决方法, 用于应对功能不可用的特定情况. - // ! Compatibility (兼容) 是一种更广泛的设计原则, 确保新版本能兼容并继续支持旧版本的操作和数据. - val fallback = "fallback" - val upgraded = "upgraded" val downgraded = "downgraded" val nearestLowerMatched = "nearest-lower-matched" + val fallback = "fallback" val specified = "specified" val auto = "auto" val toml = "toml" - val fallbackSuffix = " [$fallback]" val upgradedSuffix = " [$upgraded]" val downgradedSuffix = " [$downgraded]" val nearestLowerMatchedSuffix = " [$nearestLowerMatched]" + val fallbackSuffix = " [$fallback]" val autoSpecifiedSuffix = " [$auto-$specified]" val userSpecifiedSuffix = " [user-$specified]" val tomlSpecifiedSuffix = " [toml-$specified]" @@ -704,30 +499,30 @@ pluginManagement { private val versions = object { - val agp = object : Version(platform.agpVersionMap, platform.version, config.fallbackAgpVersion) { + val agp = object : Version(platform.agpVersionMap, platform.version) { override fun refinedBestMatchingValue(bestMatchingValue: String?): String? { val currentGradleVersion = gradle.gradleVersion.toGradleVersion() - val sorted = agpGradleCompatibility.sortedByDescending { it.second.toGradleVersion() } + val entries = agpGradleCompatProps.entries.sortedByDescending { it.value.toGradleVersion() } var maxSupportedAgpVersionPrefix: String? = null - for (pair in sorted) { - maxSupportedAgpVersionPrefix = pair.first - if (currentGradleVersion >= pair.second.toGradleVersion()) { + for ((agp, minRequiredGradle) in entries) { + maxSupportedAgpVersionPrefix = agp + if (currentGradleVersion >= minRequiredGradle.toGradleVersion()) { break } } - val maxSupporedAgpVersion = maxSupportedAgpVersionPrefix?.let { getAgpReleasedVersion(it) } - maxSupporedAgpVersion ?: return bestMatchingValue - bestMatchingValue ?: return maxSupporedAgpVersion.also { + val maxSupportedAgpVersion = maxSupportedAgpVersionPrefix?.let { getAgpReleasedVersion(it) } + maxSupportedAgpVersion ?: return bestMatchingValue + bestMatchingValue ?: return maxSupportedAgpVersion.also { bestMatchingOperationHintSuffix += identifier.autoSpecifiedSuffix } if (!bestMatchingValue.contains("\\d+\\.\\d+\\.\\d+".toRegex())) { bestMatchingOperationHintSuffix += identifier.autoSpecifiedSuffix return getAgpReleasedVersion(bestMatchingValue) ?: "$bestMatchingValue.0" } - if (utils.compareVersionStrings(bestMatchingValue, maxSupporedAgpVersion) > 0) { + if (utils.compareVersionStrings(bestMatchingValue, maxSupportedAgpVersion) > 0) { bestMatchingOperationHintSuffix += identifier.downgradedSuffix - return maxSupporedAgpVersion + return maxSupportedAgpVersion } return bestMatchingValue } @@ -739,14 +534,14 @@ pluginManagement { } - val kotlin = object : Version(emptyMap(), platform.version, config.fallbackKotlinVersion) { - + val kotlin = object : Version( + gradleKotlinCompatProps.filter { (gradleMin, _) -> + gradle.gradleVersion.toGradleVersion() >= gradleMin.toGradleVersion() + }.toSortedMap(utils::compareVersionStrings).reversed(), + platform.version, + ) { override fun refinedBestMatchingValue(bestMatchingValue: String?): String? { - val currentGradleVersion = gradle.gradleVersion.toGradleVersion() - val kotlinMin = gradleKotlinCompatibility - .filter { (gradleMin, _) -> currentGradleVersion >= gradleMin.toGradleVersion() } - .maxByOrNull { it.first.toGradleVersion() } - ?.second + val kotlinMin = map.maxByOrNull { it.key.toGradleVersion() }?.value return when { kotlinMin == null -> bestMatchingValue bestMatchingValue == null -> { @@ -772,24 +567,31 @@ pluginManagement { "ksp" to { versionMap: Map -> var suffix = "" - val kotlinVersion = config.libs.filterIsInstance() - .find { it.id.contains("kotlin-gradle-plugin", true) } - ?.takeIf { !it.version.startsWith("${identifier.auto}:") } - ?.version?.let { kt -> - versionMap[kt]?.let { kt } - } - ?: versions.kotlin.bestMatchingValue?.also { - suffix += identifier.autoSpecifiedSuffix - } - ?: versions.kotlin.fallbackVersion?.let { kt -> - suffix += identifier.fallbackSuffix - versionMap[kt]?.let { kt } - } + val kotlinVersion = run { + val kotlinClasspath = config.libs.filterIsInstance().find { + it.id.contains("kotlin-gradle-plugin", true) + } ?: throw Exception("Failed to find classpath for Kotlin Gradle Plugin") - val kspVersion = kotlinVersion?.let { kt -> - val ver = Version(config.kspVersionMap, kt) - val key = ver.bestMatchingKey ?: return@let null - val value = ver.bestMatchingValue ?: return@let null + if (!kotlinClasspath.version.startsWith("${identifier.auto}:")) { + if (kotlinClasspath.version in versionMap) { + return@run kotlinClasspath.version + } + throw Exception("Failed to find Kotlin Gradle Plugin version for \"${kotlinClasspath.version}\"") + } + versions.kotlin.bestMatchingValue?.let { + suffix += identifier.autoSpecifiedSuffix + return@run it + } + versionMap.keys.minWithOrNull(utils::compareVersionStrings)?.let { + suffix += identifier.fallbackSuffix + return@run it + } ?: throw Exception("Failed to determine Kotlin Gradle Plugin version for \"${kotlinClasspath.version}\"") + } + + val kspVersion = run { + val ver = Version(kspReleaseProps, kotlinVersion) + val key = ver.bestMatchingKey ?: return@run null + val value = ver.bestMatchingValue ?: return@run null suffix += ver.bestMatchingOperationHintSuffix "$key-$value" } @@ -833,9 +635,9 @@ pluginManagement { } ver.bestMatchingValue?.also { suffix += ver.bestMatchingOperationHintSuffix - } ?: ver.fallbackVersion?.also { + } ?: ver.map.values.minWithOrNull(utils::compareVersionStrings)?.also { suffix += identifier.fallbackSuffix - } ?: consts.DEFAULT_VERSION + } ?: throw Exception("Failed to determine version for classpath \"${lib.id}\"") } lib.version.startsWith("${identifier.toml}:") -> { toml.getValue(lib.version.removePrefix("${identifier.toml}:")).also { @@ -860,7 +662,7 @@ pluginManagement { val version: String = when { lib.version.startsWith("${identifier.auto}:") -> { val automator = pluginVersionAutomatorMap.getValue(lib.version.removePrefix("${identifier.auto}:")) - val result = automator.invoke(config.kspVersionMap) + val result = automator.invoke(kspReleaseProps) result["suffix"]?.let { s -> suffix += s } result["version"] ?: throw Exception("Unknown version for plugin ${lib.id}") } @@ -878,7 +680,7 @@ pluginManagement { mapOf("id" to lib.id, "version" to version, "isApply" to lib.isApply) } - private open inner class Version(val map: Map, platformVersion: String, val fallbackVersion: String? = null) { + private open inner class Version(val map: Map, platformVersion: String) { val bestMatchingKey: String? = findBestMatchingMapKey(platformVersion) @@ -893,7 +695,7 @@ pluginManagement { fun findBestMatchingMapKey(platformVersion: String): String? { val (platformVersionNumbers, platformVersionSuffix) = utils.toVersionParts(platformVersion) - val sortedVersions = map.keys.filter { it != identifier.fallback }.sortedWith(utils::compareVersionStrings).reversed() + val sortedVersions = map.keys.sortedWith(utils::compareVersionStrings).reversed() for (version in sortedVersions) { val (versionNumbers, versionSuffix) = utils.toVersionParts(version) val versionComparisonScore = utils.compareVersionParts(versionNumbers, platformVersionNumbers) @@ -920,7 +722,9 @@ pluginManagement { fun parseVersion(version: String) = version.split(Regex("[.-]")).map { it.toIntOrNull() ?: 0 } val inputGradleVersionInts = parseVersion(gradleVersion) - val sortedJavaGradleCompatibility = javaGradleCompatibility.sortedBy { it.first } + val sortedJavaGradleCompatibility = javaGradleCompatProps.entries.map { + it.key.toInt() to it.value + }.sortedBy { it.first } var maxJavaVersion: Int = sortedJavaGradleCompatibility.first().first diff --git a/version.properties b/version.properties index d8758cf3..4f87e7a2 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,5 @@ -#Wed Oct 01 00:07:05 CST 2025 -BUILD_TIME=1759248425157 +#Thu Oct 02 12:47:18 CST 2025 +BUILD_TIME=1759380438871 COMPILE_SDK_VERSION=36 IMAGE_QUANT_CMAKE_VERSION=3.22.1 IMAGE_QUANT_NDK_VERSION=26.1.10909125 @@ -12,6 +12,10 @@ MIN_SUPPORTED_ANDROID_STUDIO_IDE_VERSION=2023.3 MIN_SUPPORTED_GRADLE_VERSION=8.2 MIN_SUPPORTED_INTELLIJ_IDEA_AGP_VERSION=8.2 MIN_SUPPORTED_INTELLIJ_IDEA_IDE_VERSION=2023.3 +OVERRIDDEN_ANDROID_GRADLE_PLUGIN_VERSION=NONE +OVERRIDDEN_JAVA_VERSION=NONE +OVERRIDDEN_KOTLIN_GRADLE_PLUGIN_VERSION=NONE +OVERRIDDEN_KSP_GRADLE_PLUGIN_VERSION=NONE PADDLE_OCR_CMAKE_VERSION=3.10.2 PADDLE_OCR_NDK_VERSION=21.1.6352462 PADDLE_OCR_OPENCV_VERSION=4.8.0 @@ -23,6 +27,6 @@ RAPID_OCR_OPENCV_MOBILE_LABEL_VERSION=13 RAPID_OCR_OPENCV_MOBILE_VERSION=4.5.3 TARGET_SDK_VERSION=36 TARGET_SDK_VERSION_INRT=29 -VERSION_BUILD=3390 +VERSION_BUILD=3394 VERSION_NAME=6.7.0 Alpha6 -VSCODE_EXT_REQUIRED_VERSION=1.0.8 +VSCODE_EXT_REQUIRED_VERSION=1.0.8 \ No newline at end of file