From b83ce13d0ef33f64abacb8ebae7a8c63b100163b Mon Sep 17 00:00:00 2001 From: SuperMonster003 Date: Thu, 9 Oct 2025 17:13:48 +0800 Subject: [PATCH] =?UTF-8?q?6.7.0=20-=20Alpha6=20-=20=E6=96=B0=E5=A2=9E=20A?= =?UTF-8?q?ndroid=20=E5=8F=91=E8=A1=8C=E7=89=88=E6=9C=AC=20scraper=20?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E5=8F=8A=E4=BF=A1=E6=81=AF=E6=89=93=E5=8D=B0?= =?UTF-8?q?=E8=84=9A=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- .utils/declarations/index.d.ts | 15 + .utils/fetch-and-parse-android-releases.mjs | 230 +++++++++++++++ .utils/print-android-version-codes.bat | 32 +++ .utils/print-android-version-codes.mjs | 110 +++++++ .utils/run-scrapers.mjs | 1 + .utils/scrape-and-update-android-releases.mjs | 268 ++++++++++++++++++ .../src/main/resources/version-codes.txt | 40 +++ gradle/data/ksp-releases.properties | 8 +- 9 files changed, 702 insertions(+), 4 deletions(-) create mode 100644 .utils/fetch-and-parse-android-releases.mjs create mode 100644 .utils/print-android-version-codes.bat create mode 100644 .utils/print-android-version-codes.mjs create mode 100644 .utils/scrape-and-update-android-releases.mjs create mode 100644 build-logic/ksp-version-codes-processor/src/main/resources/version-codes.txt diff --git a/.gitignore b/.gitignore index f6c8db8b..3d8c8cfe 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,4 @@ sync.ffs_db /*dependecies_tree.txt **/assets-app/declarations/ -**/assets-app/sample/declarations/ \ No newline at end of file +**/assets-app/sample/declarations/ diff --git a/.utils/declarations/index.d.ts b/.utils/declarations/index.d.ts index 0e7b2829..5657b0a8 100644 --- a/.utils/declarations/index.d.ts +++ b/.utils/declarations/index.d.ts @@ -1,3 +1,6 @@ +type Cheerio = import('cheerio').Cheerio; +type DomElement = import('domhandler').Element; + type Page = import('puppeteer').Page; type Frame = import('puppeteer').Frame; type ElementHandle = import('puppeteer').ElementHandle; @@ -13,6 +16,18 @@ type TableDataStructureItemName = string; type TableDataStructureItem = RegExp | ((s: string) => boolean | string); type TableDataStructureItemForPageEvaluate = string | RegExp | ((s: string) => boolean | string); +interface AndroidReleaseWiki { + RELEASE_NAME: string; + INTERNAL_CODENAME: string; + PLATFORM_VERSION: string; + API_LEVEL: number; + RELEASE_DATE: string; +} + +interface AndroidReleaseCsv extends AndroidReleaseWiki { + VERSION_CODE: string; +} + interface AndroidStudioRelease { content: { item: AndroidStudioReleaseItem[]; diff --git a/.utils/fetch-and-parse-android-releases.mjs b/.utils/fetch-and-parse-android-releases.mjs new file mode 100644 index 00000000..aaeccde7 --- /dev/null +++ b/.utils/fetch-and-parse-android-releases.mjs @@ -0,0 +1,230 @@ +// fetch-and-parse-android-releases.mjs + +import * as cheerio from 'cheerio'; +import fetch from 'node-fetch'; +import { fileURLToPath } from 'node:url'; + +const versionCodeBlacklist = [ 'CUR_DEVELOPMENT' ]; + +const WIKI_URL = 'https://en.wikipedia.org/wiki/Android_version_history'; +const ANDROID_VC_URL = 'https://developer.android.com/reference/android/os/Build.VERSION_CODES'; + +const norm = (/** @type {string} */ s) => (s ?? '').trim().replace(/\s+/g, ' '); +const trimAndNormDash = (/** @type {string} */ s) => s.replace(/\s*[\u002d\u2013\u2014]\s*/g, '-'); +const stripSupportPrefix = (/** @type {string} */ s) => s.replace(/^(un)?supported:\s*/ig, ''); +const handleCsvEmptyString = (/** @type {string} */ s) => /^-?$/.test(s) ? '""' : s; + +/** + * @param {AndroidReleaseCsv} o + * @returns {AndroidReleaseCsv} + */ +function quoteIfNeeded(o) { + Object.keys(o).forEach((k) => { + if (typeof o[k] === 'string') { + o[k] = handleCsvEmptyString(o[k]); + } + }); + return o; +} + +/** + * Remove style, script, and sup.reference elements before getting text.
+ * zh-CN: 获取文本前, 先移除 style, script 及 sup.reference. + * + * @param {Cheerio} cell + * @returns {string} + */ +function cleanCell(cell) { + const cloned = cell.clone(); + cloned.find('style,script,sup.reference').remove(); + return cloned.text().trim().replace(/\s+/g, ' '); +} + +/** + * Fetch official VERSION_CODE and API_LEVEL from Android documentation.
+ * zh-CN: 抓取官方 VERSION_CODE 与 API_LEVEL. + * + * @example Map + * @returns {Promise>} + */ +async function fetchOfficialVC() { + const html = await (await fetch(ANDROID_VC_URL)).text(); + const $ = cheerio.load(html); + + /** + * @example Map + * @type {Map} + */ + const map = new Map(); + $('h3.api-name').each((_i, h3) => { + const $h3 = $(h3); + const versionCode = norm($h3.attr('id') || $h3.text()); + if (!versionCode || versionCodeBlacklist.includes(versionCode.toUpperCase())) return; + + const container = $h3.parent(); + const apiLevelText = container.find('.api-level').first().text(); + const apiLevel = (/* @IIFE */ () => { + // "Constant Value: X" + const constValMatched = container.text().match(/Constant\s+Value:\s*(\d+)/i); + if (constValMatched) return parseInt(constValMatched[1], 10); + // "Added in API level X" + const addedInMatched = apiLevelText.match(/API level\s+(\d+)/i); + if (addedInMatched) return parseInt(addedInMatched[1], 10); + })(); + if (apiLevel && Number.isFinite(apiLevel)) { + if (!map.has(apiLevel)) { + map.set(apiLevel, versionCode); + } + } + }); + + return map; +} + +/** + * Fetch all fields except VERSION_CODE from Wikipedia.
+ * zh-CN: 抓取维基除 VERSION_CODE 外的字段. + * + * @returns {Promise} + */ +async function fetchWikiRows() { + const $ = cheerio.load(await (await fetch(WIKI_URL)).text()); + + const re = /^(Name|Internal\s*codename|Version\s*number(\(s\)|s)?|API\s*level|Release\s*date)$/i; + const table = $('table.wikitable').filter((_, table) => { + return Array.from($(table).find('tr th')).filter(th => { + const extractedText = $(th).text().replace(/\[\d+]/g, '').trim(); + return re.test(extractedText); + }).length >= 3; + }).first(); + + /** @type {AndroidReleaseWiki[]} */ + const rows = []; + + /** + * Maintain column state.
+ * zh-CN: 维护列状态. + * + * @example Record + * { 2: { text: "Gingerbread"; rowSpansLeft: 3 } } + * + * @type {Record; rowSpansLeft: number }>} + */ + const rowSpansState = {}; + + table.find('tr').each((_i, tr) => { + const $tr = $(tr); + const tds = $tr.find('td'); + if (!tds.length) return; + + /** @type {Cheerio[]} */ + const cells = []; + + let chosenTdsIdx = 0; + + for (let i = 0; i < 5; i += 1) { + let cell = null; + if (i in rowSpansState) { + cell = rowSpansState[i].cell; + cells.push(cell); + rowSpansState[i].rowSpansLeft -= 1; + } else { + cell = tds.eq(chosenTdsIdx++); + if (!cell || cell.length === 0) return; + cells.push(cell); + const rowSpansLeft = parseInt(cell.attr('rowspan') || '1', 10) - 1; + if (rowSpansLeft > 0) { + rowSpansState[i] = { cell, rowSpansLeft }; + } + } + } + + const [ nameCell, codenameCell, versionCell, apiCell, dateCell ] = cells; + + const name = cleanCell(nameCell); + const codename = trimAndNormDash(cleanCell(codenameCell)); + + // Version prefers data-sort-value (can be on current td or its child elements). + // zh-CN: Version 优先使用 data-sort-value (可在当前 td 或其子元素上). + const sortVal = versionCell.attr('data-sort-value') || versionCell.find('[data-sort-value]').attr('data-sort-value'); + const versionText = trimAndNormDash(stripSupportPrefix(sortVal || cleanCell(versionCell))); + + const api = cleanCell(apiCell); + const date = cleanCell(dateCell); + + rows.push({ + RELEASE_NAME: name, + INTERNAL_CODENAME: codename, + PLATFORM_VERSION: versionText, + API_LEVEL: parseInt(api, 10), + RELEASE_DATE: date, + }); + + for (const k of Object.keys(rowSpansState)) { + if (rowSpansState[k].rowSpansLeft === 0) { + delete rowSpansState[k]; + } + } + }); + + return rows; +} + +/** + * @returns {Promise} + */ +export async function fetchAndroidReleases() { + const official = await fetchOfficialVC(); + const wiki = await fetchWikiRows(); + + /** @type {Map} */ + const mergedByApi = new Map(); + + // First, populate with wiki data. + // zh-CN: 先放入 wiki 数据. + for (const r of wiki) { + const api = r.API_LEVEL; + if (!Number.isFinite(api)) continue; + const prev = mergedByApi.get(api) || { + VERSION_CODE: '', RELEASE_NAME: '', INTERNAL_CODENAME: '', + PLATFORM_VERSION: '', API_LEVEL: api, RELEASE_DATE: '', + }; + mergedByApi.set(api, quoteIfNeeded({ + ...prev, + RELEASE_NAME: r.RELEASE_NAME || prev.RELEASE_NAME, + INTERNAL_CODENAME: r.INTERNAL_CODENAME || prev.INTERNAL_CODENAME, + PLATFORM_VERSION: r.PLATFORM_VERSION || prev.PLATFORM_VERSION, + RELEASE_DATE: r.RELEASE_DATE || prev.RELEASE_DATE, + })); + } + + // Override VERSION_CODE with official data. + // zh-CN: 用 official 覆盖 VERSION_CODE. + for (const [ api, versionCode ] of official.entries()) { + const prev = mergedByApi.get(api) || { + VERSION_CODE: '', RELEASE_NAME: '', INTERNAL_CODENAME: '', + PLATFORM_VERSION: '', API_LEVEL: api, RELEASE_DATE: '', + }; + mergedByApi.set(api, { ...prev, VERSION_CODE: versionCode, API_LEVEL: api }); + } + + // Output: Sort by API_LEVEL in descending order, + // keep only records with VERSION_CODE (officially released/named). + // zh-CN: 输出: 按 API_LEVEL 降序, 仅保留存在 VERSION_CODE 的记录 (即官方已发布/命名的). + return Array.from(mergedByApi.values()) + .filter(r => r.VERSION_CODE) + .sort((a, b) => b.API_LEVEL - a.API_LEVEL); +} + +async function main() { + console.table(await fetchAndroidReleases()); +} + +// 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/print-android-version-codes.bat b/.utils/print-android-version-codes.bat new file mode 100644 index 00000000..681143c6 --- /dev/null +++ b/.utils/print-android-version-codes.bat @@ -0,0 +1,32 @@ +@ECHO OFF + +:RUN +node "print-android-version-codes.mjs" +ECHO. + +ECHO Press [R] to rerun, [Shift+R] to clear screen then rerun, or [ESC]/[Enter]/[Space] to exit... +powershell -NoLogo -NoProfile -Command ^ + "$ErrorActionPreference='Stop';" ^ + "while($true){" ^ + " $k=$Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown');" ^ + " if($k.VirtualKeyCode -eq 27 -or $k.VirtualKeyCode -eq 13 -or $k.Character -eq ' '){ exit 1 }" ^ + " elseif($k.VirtualKeyCode -eq 82){" ^ + " $isShift = ($k.ControlKeyState -band 0x0010) -ne 0 -or ($k.ControlKeyState -band 0x0080) -ne 0;" ^ + " if($isShift){ exit 2 } else { exit 0 }" ^ + " }" ^ + "}" + +IF ERRORLEVEL 2 GOTO SHIFT_R +IF ERRORLEVEL 1 GOTO EXIT + +echo. +GOTO RUN + +:SHIFT_R +CLS +echo. +GOTO RUN + +:EXIT +echo. +EXIT /B \ No newline at end of file diff --git a/.utils/print-android-version-codes.mjs b/.utils/print-android-version-codes.mjs new file mode 100644 index 00000000..930a1e05 --- /dev/null +++ b/.utils/print-android-version-codes.mjs @@ -0,0 +1,110 @@ +// print-android-version-codes.mjs + +import * as fsp from 'node:fs/promises'; +import * as path from 'path'; + +const CSV_PATH = path.resolve(process.cwd(), '../build-logic/ksp-version-codes-processor/src/main/resources/version-codes.csv'); + +/** + * Simple CSV parsing: supports double quoted fields, commas and line breaks.
+ * zh-CN: 简易 CSV 解析: 支持双引号字段, 逗号, 换行. + * + * @param {string} text + * @returns {string[][]} + */ +function parseCsv(text) { + /** @type {string[][]} */ + const rows = []; + /** @type {string[]} */ + const row = []; + + let i = 0; + let field = ''; + let inQuotes = false; + + const pushField = () => { + row.push(field); + field = ''; + }; + const pushRow = () => { + if (row.length) rows.push(row.slice()); + row.splice(0); + }; + + while (i < text.length) { + const c = text[i]; + if (inQuotes) { + if (c === '"') { + if (i + 1 < text.length && text[i + 1] === '"') { + field += '"'; + i += 2; + continue; + } + inQuotes = false; + i++; + continue; + } + field += c; + i++; + continue; + } + if (c === '"') { + inQuotes = true; + i++; + continue; + } + if (c === ',') { + pushField(); + i++; + continue; + } + if (c === '\r') { + i++; + continue; + } + if (c === '\n') { + pushField(); + pushRow(); + i++; + continue; + } + field += c; + i++; + } + // Last cell or last row. + // zh-CN: 最后一格或最后一行. + if (field.length || row.length) { + pushField(); + pushRow(); + } + return rows; +} + +/** + * @param {string[][]} rows + * @returns {Object[]} + */ +function toObjects(rows) { + if (!rows.length) return []; + /** @type {string[]} */ + const header = rows[0].map(h => h.trim()); + return rows.slice(1).map(r => { + /** @type {Object} */ + const o = {}; + for (let i = 0; i < header.length; i++) { + const key = header[i]; + const value = (r[i] ?? '').trim(); + o[key] = key === 'API_LEVEL' ? parseInt(value, 10) : value; + } + return o; + }); +} + +(async function main() { + const text = await fsp.readFile(CSV_PATH, 'utf8'); + const objs = toObjects(parseCsv(text)); + console.table(objs); +})().catch(err => { + console.error(err); + process.exitCode = 1; +}); diff --git a/.utils/run-scrapers.mjs b/.utils/run-scrapers.mjs index 1f9a623a..7db6f9cd 100644 --- a/.utils/run-scrapers.mjs +++ b/.utils/run-scrapers.mjs @@ -20,6 +20,7 @@ const SCRIPT_LIST = [ 'scrape-and-inject-agp-releases-list.mjs', 'scrape-and-inject-ksp-releases-map.mjs', 'scrape-and-update-foojay-resolver-version.mjs', + 'scrape-and-update-android-releases.mjs', 'scrape-and-update-readme-template-contributors-table.mjs', ]; diff --git a/.utils/scrape-and-update-android-releases.mjs b/.utils/scrape-and-update-android-releases.mjs new file mode 100644 index 00000000..82c07c89 --- /dev/null +++ b/.utils/scrape-and-update-android-releases.mjs @@ -0,0 +1,268 @@ +// scrape-and-update-android-releases.mjs + +import * as fs from 'fs'; +import * as path from 'path'; +import { fetchAndroidReleases } from './fetch-and-parse-android-releases.mjs'; +import { printLinesDiffs } from './utils/print.mjs'; + +const CSV_PATH = path.resolve(process.cwd(), '../build-logic/ksp-version-codes-processor/src/main/resources/version-codes.csv'); +const TXT_PATH = path.resolve(process.cwd(), '../build-logic/ksp-version-codes-processor/src/main/resources/version-codes.txt'); + +const readFileSafe = (/** @type {string} */ p) => (fs.existsSync(p) ? fs.readFileSync(p, 'utf8') : ''); + +const quoteCsv = (/** @type {string | number} */ v) => { + const s = String(v ?? ''); + if (s === '""') return s; + return /[",\r\n]/.test(s) ? `"${s.replace(/"/g, '""')}"` : s; +}; + +/** + * @param {AndroidReleaseCsv[]} rows + * @returns {string} + */ +const toCsv = (rows) => { + /** @type {(keyof AndroidReleaseCsv)[]} */ + const headers = [ + 'VERSION_CODE', + 'RELEASE_NAME', + 'INTERNAL_CODENAME', + 'PLATFORM_VERSION', + 'API_LEVEL', + 'RELEASE_DATE', + ]; + const lines = rows.map(r => headers.map(k => quoteCsv(r[k])).join()); + return [ headers.join(), ...lines ].join('\n'); +}; + +/** + * @param {AndroidReleaseCsv[]} data + * @returns {void} + */ +function updateCsv(data) { + fs.mkdirSync(path.dirname(CSV_PATH), { recursive: true }); + fs.writeFileSync(CSV_PATH, toCsv(data), 'utf8'); +} + +/** + * @param {AndroidReleaseCsv[]} data + * @param {object} options + * @param {boolean} [options.showIndex=false] + * @param {{ mode: 'all' | 'not-all-numbers' | 'none', style: 'single' | 'double' }} [options.quote] + * @param {{ key: string, compare: (a: AndroidReleaseCsv, b: AndroidReleaseCsv) => number } | { key: string, direction: 'asc' | 'desc' }} [options.sortBy] + * @param {{ [key: string]: string }} [options.headerMap] + * @param {'lower' | 'upper' | 'snake' | 'kebab' | 'title' | 'camel' | 'pascal' | null} [options.headerCase] + * @param {string[]} [options.headers] + * @param {{ [key: string]: (v: any, row: AndroidReleaseCsv, header: string) => string }} [options.renderers] + * @returns {void} + */ +function updateTxt(data, options) { + const defaultOptions = { + showIndex: false, + quote: { + mode: 'not-all-numbers', + style: 'single', + }, + sortBy: null, + headerMap: null, + headerCase: null, + headers: [ + 'VERSION_CODE', + 'RELEASE_NAME', + 'INTERNAL_CODENAME', + 'PLATFORM_VERSION', + 'API_LEVEL', + 'RELEASE_DATE', + ], + renderers: {}, + }; + const opts = Object.assign(defaultOptions, options ?? {}); + + const headers = opts.headers.slice(); + + const toWords = (/** @type {any} */ s) => String(s ?? '') + .replace(/[_\-\s]+/g, ' ') + .replace(/([a-z0-9])([A-Z])/g, '$1 $2') + .trim() + .split(/\s+/); + + /** + * @param {any} s + * @returns {string} + */ + const toCase = (s) => { + const words = toWords(s); + switch (opts.headerCase) { + case 'lower': + return words.join(' ').toLowerCase(); + case 'upper': + return words.join(' ').toUpperCase(); + case 'snake': + return words.map(w => w.toLowerCase()).join('_'); + case 'kebab': + return words.map(w => w.toLowerCase()).join('-'); + case 'title': + return words.map(w => w[0] ? (w[0].toUpperCase() + w.slice(1).toLowerCase()) : w).join(' '); + case 'camel': + return words.map((w, i) => i === 0 ? w.toLowerCase() : (w[0]?.toUpperCase() + w.slice(1).toLowerCase())).join(''); + case 'pascal': + return words.map(w => w[0]?.toUpperCase() + w.slice(1).toLowerCase()).join(''); + default: + return s; + } + }; + + /** + * Copy and sort data.
+ * zh-CN: 数据拷贝与排序. + * @type {AndroidReleaseCsv[]} + */ + const rows = data.slice(); + if (opts.sortBy) { + if (typeof opts.sortBy === 'function') { + rows.sort(opts.sortBy); + } else if (opts.sortBy && typeof opts.sortBy === 'object' && opts.sortBy.key) { + const key = opts.sortBy.key; + const dir = (opts.sortBy.direction || 'asc').toLowerCase() === 'desc' ? -1 : 1; + const cmp = typeof opts.sortBy.compare === 'function' + ? opts.sortBy.compare + : (/** @type {AndroidReleaseCsv} */ a, /** @type {AndroidReleaseCsv} */ b) => { + const va = a?.[key]; + const vb = b?.[key]; + if (va == null && vb == null) return 0; + if (va == null) return -1; + if (vb == null) return 1; + if (va < vb) return -1; + if (va > vb) return 1; + return 0; + }; + rows.sort((a, b) => dir * cmp(a, b)); + } + } + + /** + * Prepare column values (strings) and determine column quotation in "not-all-numbers" mode.
+ * zh-CN: 先把每列值准备好 (字符串), 并在 "not-all-numbers" 模式下做整列判定. + * @type {string[][]} + */ + const tableValues = rows.map(r => { + return headers.map(h => { + const v = r[h]; + const renderer = opts.renderers?.[h]; + const rendered = renderer ? renderer(v, r, h) : (v ?? ''); + return String(rendered); + }); + }); + + /** + * Column headers (remapping + case transformation + affixes).
+ * zh-CN: 列标题 (重映射 + 大小写 + 前后缀). + * @type {string[]} + */ + const headerLabels = headers.map(h => { + const mapped = opts.headerMap?.[h] ?? h; + return toCase(mapped); + }); + const finalHeader = opts.showIndex ? [ '(index)', ...headerLabels ] : headerLabels; + + const qStyle = opts.quote?.style === 'double' ? `"` : `'`; + const isNumericLike = (/** @type {any} */ v) => /^-?\d+(\.\d+)?$/.test(String(v ?? '').trim()); + + /** @type {boolean[]} */ + const quotedColumns = tableValues[0] ? new Array(tableValues[0].length).fill(false) : []; + if (opts.quote?.mode === 'all') { + for (let c = 0; c < quotedColumns.length; c++) { + quotedColumns[c] = true; + } + } else if (opts.quote?.mode === 'not-all-numbers' || !opts.quote?.mode) { + for (let c = 0; c < quotedColumns.length; c++) { + const col = tableValues.map(row => row[c]); + const allNumeric = col.length > 0 && col.every(isNumericLike); + quotedColumns[c] = !allNumeric; + } + } + + /** + * Apply quotes to data.
+ * zh-CN: 应用引号到数据. + * @type {string[][]} + */ + const quotedTable = tableValues.map(row => { + return row.map((cell, c) => quotedColumns[c] + ? qStyle + cell + qStyle + : cell === `""` ? qStyle + qStyle : cell); + }); + + /** + * Calculate the width of each column.
+ * zh-CN: 计算每列宽度. + * @type {string[][]} + */ + const rowsForWidth = []; + rowsForWidth.push(finalHeader); + if (opts.showIndex) { + for (let i = 0; i < quotedTable.length; i++) { + rowsForWidth.push([ String(i), ...quotedTable[i] ]); + } + } else { + rowsForWidth.push(...quotedTable); + } + const colCount = rowsForWidth[0]?.length ?? 0; + /** @type {number[]} */ + const colWidths = new Array(colCount).fill(0); + for (const r of rowsForWidth) { + for (let c = 0; c < colCount; c++) { + colWidths[c] = Math.max(colWidths[c], String(r[c] ?? '').length); + } + } + + /** + * @param {any} s + * @param {number} w + * @returns {string} + */ + const pad = (s, w) => { + const str = String(s ?? ''); + const len = str.length; + return str + (len < w ? ' '.repeat(w - len) : ''); + }; + const joinRow = (/** @type {any[]} */ cells) => `| ${cells.map((v, i) => pad(v, colWidths[i])).join(' | ')} |`; + const divider = `+-${colWidths.map(w => '-'.repeat(w)).join('-+-')}-+`; + + const lines = []; + lines.push(divider); + lines.push(joinRow(finalHeader)); + lines.push(divider); + if (quotedTable.length > 0) { + for (let i = 0; i < quotedTable.length; i++) { + const cells = opts.showIndex ? [ String(i), ...quotedTable[i] ] : quotedTable[i]; + lines.push(joinRow(cells)); + } + } + lines.push(divider); + + fs.mkdirSync(path.dirname(TXT_PATH), { recursive: true }); + fs.writeFileSync(TXT_PATH, lines.join('\n'), 'utf8'); +} + +(async function main() { + const filename = path.basename(CSV_PATH); + const releases = await fetchAndroidReleases(); + const prev = readFileSafe(CSV_PATH).replaceAll('\r\n', '\n'); + if (prev !== toCsv(releases)) { + updateCsv(releases); + updateTxt(releases, { + showIndex: false, + quote: { + mode: 'none', + style: 'single', + }, + }); + console.log(`[${filename}] Updated (Android releases)`); + printLinesDiffs(prev, toCsv(releases)); + } else { + // console.log('[${filename}] No update needed (Android releases)'); + } +})().catch(err => { + console.error(err); + process.exitCode = 1; +}); diff --git a/build-logic/ksp-version-codes-processor/src/main/resources/version-codes.txt b/build-logic/ksp-version-codes-processor/src/main/resources/version-codes.txt new file mode 100644 index 00000000..1fb92f02 --- /dev/null +++ b/build-logic/ksp-version-codes-processor/src/main/resources/version-codes.txt @@ -0,0 +1,40 @@ ++------------------------+----------------------------+----------------------+------------------+-----------+--------------------+ +| VERSION_CODE | RELEASE_NAME | INTERNAL_CODENAME | PLATFORM_VERSION | API_LEVEL | RELEASE_DATE | ++------------------------+----------------------------+----------------------+------------------+-----------+--------------------+ +| BAKLAVA | Android 16 | Baklava | 16 | 36 | June 10, 2025 | +| VANILLA_ICE_CREAM | Android 15 | Vanilla Ice Cream | 15 | 35 | September 3, 2024 | +| UPSIDE_DOWN_CAKE | Android 14 | Upside Down Cake | 14 | 34 | October 4, 2023 | +| TIRAMISU | Android 13 | Tiramisu | 13 | 33 | August 15, 2022 | +| S_V2 | Android 12L | Snow Cone v2 | 12.1 | 32 | March 7, 2022 | +| S | Android 12 | Snow Cone | 12 | 31 | October 4, 2021 | +| R | Android 11 | Red Velvet Cake | 11 | 30 | September 8, 2020 | +| Q | Android 10 | Quince Tart | 10 | 29 | September 3, 2019 | +| P | Android Pie | Pistachio Ice Cream | 9 | 28 | August 6, 2018 | +| O_MR1 | Android Oreo | Oatmeal Cookie | 8.1 | 27 | December 5, 2017 | +| O | Android Oreo | Oatmeal Cookie | 8.0 | 26 | August 21, 2017 | +| N_MR1 | Android Nougat | New York Cheesecake | 7.1-7.1.2 | 25 | October 4, 2016 | +| N | Android Nougat | New York Cheesecake | 7.0 | 24 | August 22, 2016 | +| M | Android Marshmallow | Macadamia Nut Cookie | 6.0-6.0.1 | 23 | September 29, 2015 | +| LOLLIPOP_MR1 | Android Lollipop | Lemon Meringue Pie | 5.1-5.1.1 | 22 | March 2, 2015 | +| LOLLIPOP | Android Lollipop | Lemon Meringue Pie | 5.0-5.0.2 | 21 | November 4, 2014 | +| KITKAT_WATCH | Android KitKat | Key Lime Pie | 4.4W-4.4W.2 | 20 | June 25, 2014 | +| KITKAT | Android KitKat | Key Lime Pie | 4.4-4.4.4 | 19 | October 31, 2013 | +| JELLY_BEAN_MR2 | Android Jelly Bean | Jelly Bean | 4.3-4.3.1 | 18 | July 24, 2013 | +| JELLY_BEAN_MR1 | Android Jelly Bean | Jelly Bean | 4.2-4.2.2 | 17 | November 13, 2012 | +| JELLY_BEAN | Android Jelly Bean | Jelly Bean | 4.1-4.1.2 | 16 | July 9, 2012 | +| ICE_CREAM_SANDWICH_MR1 | Android Ice Cream Sandwich | Ice Cream Sandwich | 4.0.3-4.0.4 | 15 | December 16, 2011 | +| ICE_CREAM_SANDWICH | Android Ice Cream Sandwich | Ice Cream Sandwich | 4.0-4.0.2 | 14 | October 18, 2011 | +| HONEYCOMB_MR2 | Android Honeycomb | Honeycomb | 3.2-3.2.6 | 13 | July 15, 2011 | +| HONEYCOMB_MR1 | Android Honeycomb | Honeycomb | 3.1 | 12 | May 10, 2011 | +| HONEYCOMB | Android Honeycomb | Honeycomb | 3.0 | 11 | February 22, 2011 | +| GINGERBREAD_MR1 | Android Gingerbread | Gingerbread | 2.3.3-2.3.7 | 10 | February 9, 2011 | +| GINGERBREAD | Android Gingerbread | Gingerbread | 2.3-2.3.2 | 9 | December 6, 2010 | +| FROYO | Android Froyo | Froyo | 2.2-2.2.3 | 8 | May 20, 2010 | +| ECLAIR_MR1 | Android Eclair | Eclair | 2.1 | 7 | January 11, 2010 | +| ECLAIR_0_1 | Android Eclair | Eclair | 2.0.1 | 6 | December 3, 2009 | +| ECLAIR | Android Eclair | Eclair | 2.0 | 5 | October 27, 2009 | +| DONUT | Android Donut | Donut | 1.6 | 4 | September 15, 2009 | +| CUPCAKE | Android Cupcake | Cupcake | 1.5 | 3 | April 27, 2009 | +| BASE_1_1 | Android 1.1 | Petit Four | 1.1 | 2 | February 9, 2009 | +| BASE | Android 1.0 | '' | 1.0 | 1 | September 23, 2008 | ++------------------------+----------------------------+----------------------+------------------+-----------+--------------------+ \ No newline at end of file diff --git a/gradle/data/ksp-releases.properties b/gradle/data/ksp-releases.properties index 12c33119..8792b62a 100644 --- a/gradle/data/ksp-releases.properties +++ b/gradle/data/ksp-releases.properties @@ -1,6 +1,8 @@ -#Wed Oct 01 22:12:30 GMT+8 2025 -#Sep 12, 2025 -2.2.20=2.0.3 +#Thu Oct 09 11:46:44 GMT+8 2025 +#Oct 9, 2025 +2.2.21-RC=2.0.4 +#Oct 8, 2025 +2.2.20=2.0.4 #Sep 4, 2025 2.2.20-RC2=2.0.2 #Aug 20, 2025