6.7.0 - Alpha6 - 使用 Toolchain 替代 sourceCompatibility/targetCompatibility 以降低构建环境差异
This commit is contained in:
@@ -37,7 +37,8 @@
|
||||
"应用启动器图标支持自适应图标特性 _[`issue #405`](http://issues.autojs6.com/405)_",
|
||||
"Gradle 构建脚本提升 7z 格式文件的解压效率",
|
||||
"使用版本目录 (Version Catalogs) 集中管理 Gradle 依赖和插件版本",
|
||||
"模块化 Gradle 脚本, 将共享构建逻辑迁移至 buildSrc 并抽象为约定插件"
|
||||
"模块化 Gradle 脚本, 将共享构建逻辑迁移至 buildSrc 并抽象为约定插件",
|
||||
"使用 Toolchain 替代 sourceCompatibility/targetCompatibility 以降低构建环境差异"
|
||||
],
|
||||
"dependency": [
|
||||
"本地化 Root Shell 版本 1.6",
|
||||
|
||||
@@ -34,7 +34,8 @@ const childProcessOutput = [];
|
||||
* @returns {string}
|
||||
*/
|
||||
function getScriptProgressMessage(scripts, idx) {
|
||||
return `[${idx + 1}/${scripts.length}] ${scripts[idx].name}`;
|
||||
const order = `${idx + 1}`.padStart(`${scripts.length}`.length, '0');
|
||||
return `[${order}/${scripts.length}] ${scripts[idx].name}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,39 +2,70 @@
|
||||
|
||||
/** @typedef {import('puppeteer').Page} Page */
|
||||
/** @typedef {import('puppeteer').Frame} Frame */
|
||||
/** @typedef {import('./fetch-and-parse-android-studio-latest-stable-version.mjs').Row} Row */
|
||||
/** @typedef {import('./fetch-and-parse-android-studio-latest-stable-version.mjs').StableArchiveItem} Row */
|
||||
/** @typedef {import('./fetch-and-parse-android-studio-archives.mjs').ArchiveItem} ArchiveItem */
|
||||
/**
|
||||
* @template {Node} T
|
||||
* @typedef {import('puppeteer').ElementHandle<T>} ElementHandle
|
||||
*/
|
||||
|
||||
import { getLatestStableWindows } from './fetch-and-parse-android-studio-latest-stable-version.mjs';
|
||||
import * as fsp from 'node:fs/promises';
|
||||
import * as path from 'node:path';
|
||||
import { batchUpdateAnchoredBlocks } from './utils/anchors.mjs';
|
||||
import { compareVersionStrings } from './utils/versioning.mjs';
|
||||
import { toUpdatedStamp, toYYYYMMDD } from './utils/date.mjs';
|
||||
import { bytes2GiB } from './utils/format.mjs';
|
||||
import { getRemoteFileSizeBytes } from './utils/fetch.mjs';
|
||||
import { compareVersionStrings } from './utils/versioning.mjs';
|
||||
import { getAndroidStudioArchives } from './fetch-and-parse-android-studio-archives.mjs';
|
||||
import { getLatestStableArchives } from './fetch-and-parse-android-studio-latest-stable-version.mjs';
|
||||
import { getRemoteFileSizeBytes } from './utils/fetch.mjs';
|
||||
import { toUpdatedStamp, toYYYYMMDD } from './utils/date.mjs';
|
||||
|
||||
async function main() {
|
||||
// 1) 用 "最新稳定版" 的校验和/文件名, 在归档中定位条目, 补全并更新 common.json
|
||||
/**
|
||||
* Manual override for codename mappings (for resolving codename prefix conflicts).<br>
|
||||
* Default is first letter, e.g. 'Meerkat' and 'Bumblebee' gives { Meerkat: 'M', Bumblebee: 'B' }.<br>
|
||||
* When first letters conflict, conflicts are auto-resolved,
|
||||
* e.g. 'Camel' and 'Catfish' gives { Camel: 'CAM', Catfish: 'CAT' }.<br>
|
||||
* For extreme cases where conflicts cannot be auto-resolved,
|
||||
* e.g. 'Cat' and 'Catfish', manual prefix mapping is needed, like { Cat: 'CT', Catfish: 'CTF' }.<br>
|
||||
* zh-CN:<br>
|
||||
* 手动覆盖代号映射 (可用于解决代号前缀冲突).<br>
|
||||
* 默认为首字母, 如 'Meerkat' 与 'Bumblebee', 得到 { Meerkat: 'M', Bumblebee: 'B' }.<br>
|
||||
* 首字母重复时自动消解冲突, 如 'Camel' 与 'Catfish', 得到 { Camel: 'CAM', Catfish: 'CAT' }.<br>
|
||||
* 极端情况无法自动消解冲突, 如 'Cat' 与 'Catfish', 此时需要手动指定前缀, 如 { Cat: 'CT', Catfish: 'CTF' }.
|
||||
* @example Object<codename name, codename prefix>
|
||||
* {
|
||||
* 'Camel': 'CM',
|
||||
* 'Cat': 'CT',
|
||||
* 'Catfish': 'CTF',
|
||||
* ... ...
|
||||
* }
|
||||
* @type {Object<string, string>}
|
||||
*/
|
||||
const manualCodenameOverrides = {};
|
||||
|
||||
const archives = await getAndroidStudioArchives();
|
||||
const latestRows = await getLatestStableWindows(); // [{kind, filename, sha256, url, size, ...}]
|
||||
/**
|
||||
* Use the latest stable version's checksum/filename to locate the entry in archives,
|
||||
* complete and update common.json.<br>
|
||||
* zh-CN: 用 "最新稳定版" 的校验和/文件名, 在归档中定位条目, 补全并更新 common.json.
|
||||
*
|
||||
* @param {ArchiveItem[]} archives
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function updateLatestArchiveInfo(archives) {
|
||||
const latestRows = await getLatestStableArchives(); // [ { kind, filename, sha256, url, size, ... } ]
|
||||
const latestExe = latestRows.find(x => x.kind === 'exe');
|
||||
const latestZip = latestRows.find(x => x.kind === 'zip');
|
||||
const latestTar = latestRows.find(x => x.kind === 'tar');
|
||||
if (!latestExe || !latestZip || !latestTar) {
|
||||
throw new Error('最新稳定版条目缺少 Windows EXE/ZIP 或 TAR');
|
||||
throw new Error('Latest stable archives missing required "kind" info: exe, zip, or tar');
|
||||
}
|
||||
|
||||
/**
|
||||
* 在归档中查找: 优先用 sha256 命中, 其次用文件名.
|
||||
* Search the archive: first try to match by sha256, then by filename.<br>
|
||||
* zh-CN: 在归档中查找: 优先用 sha256 命中, 其次用文件名.
|
||||
*
|
||||
* @param {ArchiveItem[]} rows
|
||||
* @param {Row} target
|
||||
* @return {ArchiveItem | null}
|
||||
* @returns {ArchiveItem | null}
|
||||
*/
|
||||
const matchArchive = (rows, target) => {
|
||||
for (const arc of rows) {
|
||||
@@ -46,29 +77,29 @@ async function main() {
|
||||
};
|
||||
const matchedArc = matchArchive(archives, latestExe) || matchArchive(archives, latestZip);
|
||||
if (!matchedArc) {
|
||||
throw new Error('未能在归档中定位到与最新版本对应的条目 (按 sha256/文件名均未命中)');
|
||||
throw new Error('Could not locate entries in the archive corresponding to the latest version (neither by sha256 nor filename)');
|
||||
}
|
||||
|
||||
/**
|
||||
* 从匹配到的 expandable 中抽取 Windows EXE/ZIP 的链接/文件名.
|
||||
*
|
||||
* @param {string} suffix
|
||||
* @return {{ filename: string, url: string, sizeGiB?: string | null } | null}
|
||||
* @returns {{ filename: string, url: string, sizeGiB?: string | null } | null}
|
||||
*/
|
||||
const pickWinItem = suffix => {
|
||||
// suffix: "-windows.exe" | "-windows.zip"
|
||||
const pickWinItem = (suffix) => {
|
||||
const link = matchedArc.links.find(l => l.text.endsWith(suffix));
|
||||
if (!link) return null;
|
||||
return {
|
||||
filename: link.text,
|
||||
url: link.href,
|
||||
};
|
||||
return { filename: link.text, url: link.href };
|
||||
};
|
||||
const exeItem = pickWinItem('-windows.exe');
|
||||
const zipItem = pickWinItem('-windows.zip');
|
||||
const tarItem = pickWinItem('-linux.tar.gz');
|
||||
|
||||
// 查询真实文件大小 (并发获取), 格式化为 GiB
|
||||
if (!exeItem || !zipItem || !tarItem) {
|
||||
throw new Error('Matched archive entry missing Windows EXE/ZIP or Linux TAR download information');
|
||||
}
|
||||
|
||||
// Query real file size (concurrent fetch) and format to GiB.
|
||||
// zh-CN: 查询真实文件大小 (并发获取), 格式化为 GiB.
|
||||
|
||||
const [ exeBytes, zipBytes, tarBytes ] = await Promise.all([
|
||||
exeItem ? getRemoteFileSizeBytes(exeItem.url) : Promise.resolve(null),
|
||||
zipItem ? getRemoteFileSizeBytes(zipItem.url) : Promise.resolve(null),
|
||||
@@ -78,20 +109,17 @@ async function main() {
|
||||
if (zipItem) zipItem.sizeGiB = bytes2GiB(zipBytes);
|
||||
if (tarItem) tarItem.sizeGiB = bytes2GiB(tarBytes);
|
||||
|
||||
// 准备写回 common.json 所需字段
|
||||
const latestVersionName = matchedArc.title.trim(); // 例: "Android Studio Narwhal Feature Drop | 2025.1.2"
|
||||
const latestVersionDate = toYYYYMMDD(matchedArc.date) || ''; // 例: "2025/07/31"
|
||||
// Prepare fields needed to write back to common.json.
|
||||
// zh-CN: 准备写回 common.json 所需字段.
|
||||
|
||||
if (!exeItem || !zipItem) {
|
||||
throw new Error('匹配到的归档条目缺少 Windows EXE 或 ZIP 下载信息');
|
||||
}
|
||||
const latestVersionName = matchedArc.title.trim(); // e.g. "Android Studio Narwhal Feature Drop | 2025.1.2"
|
||||
const latestVersionDate = toYYYYMMDD(matchedArc.date) || ''; // e.g. "2025/07/31"
|
||||
|
||||
// 读取并更新 common.json (仅更新带有 "android_studio_latest_" 片段的键与版本名日期键)
|
||||
const fs = await import('node:fs/promises');
|
||||
const path = await import('node:path');
|
||||
// Read and update common.json (only update keys containing "android_studio_latest_" fragment and version name date key).
|
||||
// zh-CN: 读取并更新 common.json (仅更新带有 "android_studio_latest_" 片段的键与版本名日期键).
|
||||
|
||||
const commonJsonPath = path.resolve(process.cwd(), '../.readme/common.json');
|
||||
const commonRaw = await fs.readFile(commonJsonPath, 'utf8');
|
||||
const commonRaw = await fsp.readFile(commonJsonPath, 'utf8');
|
||||
const commonObj = JSON.parse(commonRaw);
|
||||
|
||||
const updatedCommon = {
|
||||
@@ -110,60 +138,112 @@ async function main() {
|
||||
};
|
||||
|
||||
if (JSON.stringify(updatedCommon) !== JSON.stringify(commonObj)) {
|
||||
await fs.writeFile(commonJsonPath, JSON.stringify(updatedCommon, null, 2), 'utf8');
|
||||
console.log('[common.json] 已更新 (Android Studio 数据)');
|
||||
console.log(`-- '${commonObj.android_studio_latest_recommended_version_name}'`);
|
||||
console.log(`-> '${updatedCommon.android_studio_latest_recommended_version_name}'`);
|
||||
await fsp.writeFile(commonJsonPath, JSON.stringify(updatedCommon, null, 2), 'utf8');
|
||||
console.log('[common.json] Updated (Android Studio information)');
|
||||
if (commonObj.android_studio_latest_recommended_version_name !== updatedCommon.android_studio_latest_recommended_version_name) {
|
||||
console.log(`-- '${commonObj.android_studio_latest_recommended_version_name}'`);
|
||||
console.log(`-> '${updatedCommon.android_studio_latest_recommended_version_name}'`);
|
||||
}
|
||||
} else {
|
||||
// console.log('[common.json] 无需更新 (Android Studio 数据)');
|
||||
// console.log('[common.json] No update needed (Android Studio information)');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarize codename-to-version mappings and codename first release dates.<br>
|
||||
* zh-CN: 汇总代号版本映射以及代号的首发日期.
|
||||
*
|
||||
* @param {ArchiveItem[]} archives
|
||||
*/
|
||||
function getCodenameMapLinesInfo(archives) {
|
||||
|
||||
// 2) 汇总代号 - 版本映射与代号首发日期, 更新 settings.gradle.kts 两个锚点块
|
||||
// 2.1 从标题中解析 "代号与版本号"
|
||||
// 标题模式示例: "Android Studio Meerkat Feature Drop | 2024.3.2 RC 1"
|
||||
/**
|
||||
* @param {string} t
|
||||
* @return {string | null}
|
||||
* Parse "codename and version number" from title,
|
||||
* zh-CN: 从标题中解析 "代号与版本号",
|
||||
*
|
||||
* @example string
|
||||
* 'Android Studio Meerkat Feature Drop | 2024.3.2 RC 1'
|
||||
*
|
||||
* @param {string} title
|
||||
* @returns {string | null}
|
||||
*/
|
||||
const codenameFromTitle = t => {
|
||||
// 捕获 "Android Studio <Codename> [Feature Drop] |"
|
||||
const m = /Android Studio\s+(.+?)\s*(?:(\s+\d+\s+)?Feature Drop)?\s*\|/i.exec(t);
|
||||
const codenameFromTitle = title => {
|
||||
// Capture "Android Studio <Codename> [Feature Drop]".
|
||||
// zh-CN: 捕获 "Android Studio <Codename> [Feature Drop]".
|
||||
const m = /Android Studio\s+(.+?)\s*(?:(\s+\d+\s+)?Feature Drop)?(?=\s*\|)/i.exec(title);
|
||||
return m ? m[1].trim() : null;
|
||||
};
|
||||
|
||||
// 用户手动覆盖映射 (可按需填写或从外部读取)
|
||||
/** @type {Object<string, string>} */
|
||||
const manualCodenameOverrides = {
|
||||
/* e.g. 'Meerkat': 'Mkt' */
|
||||
};
|
||||
|
||||
// 生成唯一代码: 按顺序, 出现冲突则对冲突组统一递增长度; 覆盖项优先生效且不可被自动改动
|
||||
/**
|
||||
* @param {string[]} names 原始代号 (保留大小写与空格, 顺序与站点一致)
|
||||
* @param {Object<string, string>} overrides 手动覆盖映射
|
||||
* @returns {Map<string, string>} name -> code
|
||||
* Generate unique codes: sequentially, uniformly increase length for conflict groups;
|
||||
* override mappings take precedence.<br>
|
||||
* zh-CN:<br>
|
||||
* 生成唯一代码: 按顺序, 出现冲突则对冲突组统一递增长度; 覆盖映射优先考虑.
|
||||
*
|
||||
* @example Map<name, prefixCode>
|
||||
* Map(14) {
|
||||
* 'Arctic Fox' => 'A',
|
||||
* 'Bumblebee' => 'B',
|
||||
* ... ...
|
||||
* 'Narwhal' => 'N'
|
||||
* }
|
||||
*
|
||||
* @param {string[]} names
|
||||
* Original codenames (preserves case and spaces, e.g. "Arctic Fox", "Bumblebee", "Narwhal" etc).<br>
|
||||
* zh-CN: 原始代号 (保留大小写与空格, 如 "Arctic Fox", "Bumblebee", "Narwhal" 等).
|
||||
* @param {Object<string, string>} overrides
|
||||
* Manual override mappings.<br>
|
||||
* zh-CN: 手动覆盖映射.
|
||||
* @returns {Map<string, string>}
|
||||
*/
|
||||
function buildUniquePrefixes(names, overrides) {
|
||||
// 代码不包含空格; 按名称去空格后逐字符递增长度
|
||||
const entries = names.map(n => ({
|
||||
name: n,
|
||||
base: n.replace(/\s+/g, ''), // 去空格用于截取
|
||||
len: Math.max(1, overrides[n] ? overrides[n].replace(/\s+/g, '').length : 1),
|
||||
code: overrides[n] ? overrides[n].replace(/\s+/g, '') : null,
|
||||
locked: !!overrides[n],
|
||||
}));
|
||||
const buildUniquePrefixes = (names, overrides) => {
|
||||
|
||||
// 先赋初值
|
||||
for (const e of entries) {
|
||||
if (!e.code) e.code = e.base.slice(0, e.len);
|
||||
}
|
||||
/**
|
||||
* @param {string} s
|
||||
* @returns {string}
|
||||
*/
|
||||
const normalize = (s) => s.replace(/\s+/g, '');
|
||||
|
||||
// 检测并解决冲突
|
||||
/**
|
||||
* @param {string} s
|
||||
* @param {string} def
|
||||
* @returns {string}
|
||||
*/
|
||||
const normalizeOrDefault = (s, def) => s && normalize(s) || normalize(def);
|
||||
|
||||
/**
|
||||
* @param {string }s
|
||||
* @param {number} def
|
||||
* @returns {number}
|
||||
*/
|
||||
const normalizeLength = (s, def) => s && normalize(s).length || def;
|
||||
|
||||
const entries = names.map(name => {
|
||||
const override = overrides[name]?.toUpperCase();
|
||||
const base = normalize(name);
|
||||
const len = normalizeLength(override, 1);
|
||||
const code = normalizeOrDefault(override, base.slice(0, len)).toUpperCase();
|
||||
const locked = Boolean(override);
|
||||
return { name, base, len, code, locked };
|
||||
});
|
||||
|
||||
// Detect and resolve conflicts. (zh-CN: 检测并解决冲突.)
|
||||
const maxLenByName = new Map(entries.map(e => [ e.name, e.base.length ]));
|
||||
// 循环上限保护, 防止极端情况下死循环
|
||||
for (let step = 0; step < 1024; step++) {
|
||||
// 统计冲突组: code -> indices
|
||||
/** @type {Map<string, number[]>} */
|
||||
// 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<code, indices>
|
||||
* Map(14) {
|
||||
* 'N' => [ 0 ],
|
||||
* 'M' => [ 1 ],
|
||||
* ... ...
|
||||
* 'B' => [ 12 ],
|
||||
* 'A' => [ 13 ]
|
||||
* }
|
||||
* @type {Map<string, number[]>}
|
||||
*/
|
||||
const bucket = new Map();
|
||||
entries.forEach((e, idx) => {
|
||||
const key = e.code;
|
||||
@@ -171,30 +251,26 @@ async function main() {
|
||||
bucket.get(key).push(idx);
|
||||
});
|
||||
|
||||
// 找到有冲突的组 (size >= 2)
|
||||
const conflicts = Array.from(bucket.entries()).filter(([ , indices ]) => indices.length >= 2);
|
||||
if (conflicts.length === 0) break; // 已无冲突
|
||||
const conflicts = Array.from(bucket.entries()).filter(([ _, indices ]) => indices.length >= 2);
|
||||
if (conflicts.length === 0) {
|
||||
// No conflicts, or all conflicts have been resolved.
|
||||
// zh-CN: 没有冲突, 或冲突已全部解决.
|
||||
break;
|
||||
}
|
||||
|
||||
// 逐组处理
|
||||
for (const [ code, indices ] of conflicts) {
|
||||
// 若组内存在 >=2 个锁定项且 code 相同 -> 直接报错
|
||||
const lockedIndices = indices.filter(i => entries[i].locked);
|
||||
if (lockedIndices.length >= 2) {
|
||||
const letter = code[0]?.toUpperCase() || '?';
|
||||
const groupNames = indices.map(i => entries[i].name);
|
||||
throw new Error(`[CodenameMap] 手动覆盖映射发生冲突: "${code}" -> ${groupNames.join(', ')}. 请调整覆盖映射. 冲突首字母组: ${letter}*`);
|
||||
throw new Error(`[CodenameMap] Manual override mapping conflict: "${code}" -> [ ${groupNames.join(', ')} ]`);
|
||||
}
|
||||
|
||||
// 让组内所有 "未锁定项" 统一递增长度
|
||||
for (const i of indices) {
|
||||
const e = entries[i];
|
||||
if (e.locked) continue; // 覆盖项不改
|
||||
if (e.locked) continue;
|
||||
const maxLen = maxLenByName.get(e.name);
|
||||
if (e.len >= maxLen) {
|
||||
// 已经用到全长仍冲突 -> 无法自动消解
|
||||
const letter = e.base[0]?.toUpperCase() || '?';
|
||||
const groupNames = indices.map(ii => entries[ii].name);
|
||||
throw new Error(`[CodenameMap] 无法自动消解冲突 ("${e.name}" 与同组名称完全相同或为彼此前缀到尽头). 请为该首字母组手动指定覆盖映射.\n- 组首字母: ${letter}\n- 组成员: ${groupNames.join(', ')}`);
|
||||
const groupNames = indices.map(i => entries[i].name);
|
||||
throw new Error(`[CodenameMap] Unable to auto-resolve conflicts: [ ${groupNames.join(', ')} ]`);
|
||||
}
|
||||
e.len += 1;
|
||||
e.code = e.base.slice(0, e.len);
|
||||
@@ -203,42 +279,52 @@ async function main() {
|
||||
}
|
||||
|
||||
return new Map(entries.map(e => [ e.name, e.code ]));
|
||||
}
|
||||
};
|
||||
|
||||
// 提取按页面顺序的代号列表 (去重, 保留第一次出现顺序)
|
||||
const codenamesOrdered = [];
|
||||
const seen = new Set();
|
||||
for (const arc of archives) {
|
||||
const cname = codenameFromTitle(arc.title);
|
||||
if (!cname) continue;
|
||||
if (!seen.has(cname)) {
|
||||
seen.add(cname);
|
||||
codenamesOrdered.push(cname);
|
||||
}
|
||||
}
|
||||
const codenames = [ ...new Set(archives.map(o => codenameFromTitle(o.title)).filter(Boolean)) ];
|
||||
const nameToCode = buildUniquePrefixes(codenames, manualCodenameOverrides);
|
||||
|
||||
// 构建 name->code 映射 (按规则自动消解冲突; 支持手动覆盖)
|
||||
const nameToCode = buildUniquePrefixes(codenamesOrdered, manualCodenameOverrides);
|
||||
|
||||
// 收集每版本 (yyyy.m.patch) 对应的代号集合, 以及每个代号的首发日期
|
||||
/** @type {Map<string, Set<string>>} */
|
||||
/**
|
||||
* @example Map<version, Set<prefixCode>>
|
||||
* 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<string, Set<string>>}
|
||||
*/
|
||||
const versionToLetters = new Map();
|
||||
/** @type {Map<string, { name:string, born: Date } >} */
|
||||
|
||||
/**
|
||||
* @example Map<prefixCode, { name, born }>
|
||||
* Map(14) {
|
||||
* 'N' => { name: 'Narwhal', born: 2025-03-18T16:00:00.000Z },
|
||||
* 'M' => { name: 'Meerkat', born: 2024-11-11T16:00:00.000Z },
|
||||
* ... ...
|
||||
* 'B' => { name: 'Bumblebee', born: 2021-05-17T16:00:00.000Z },
|
||||
* 'A' => { name: 'Arctic Fox', born: 2021-01-25T16:00:00.000Z }
|
||||
* }
|
||||
* @type {Map<string, { name: string, born: Date } >}
|
||||
*/
|
||||
const letterBorn = new Map();
|
||||
|
||||
for (const arc of archives) {
|
||||
if (!arc.version) continue; // 版本号 (yyyy.m.patch)
|
||||
if (!arc.version) continue;
|
||||
const cname = codenameFromTitle(arc.title);
|
||||
if (!cname) continue;
|
||||
|
||||
const code = nameToCode.get(cname);
|
||||
if (!code) continue;
|
||||
|
||||
// 映射 version -> codes
|
||||
if (!versionToLetters.has(arc.version)) versionToLetters.set(arc.version, new Set());
|
||||
versionToLetters.get(arc.version).add(code);
|
||||
|
||||
// 记录代号首次出现日期
|
||||
const d = new Date(arc.date);
|
||||
const existed = letterBorn.get(code);
|
||||
if (!existed || d < existed.born) {
|
||||
@@ -246,40 +332,100 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 生成 codenameVersionMap 文本 (按版本倒序排列, 值用 "A|B" 连接)
|
||||
const sortedVersions = Array.from(versionToLetters.keys()).sort((a, b) => {
|
||||
// Split version string into [ y: year, m: minor, p: patch ].
|
||||
// zh-CN: 将版本字符串拆分为 [ y: 年份, m: 次版本号, p: 补丁版本号 ].
|
||||
// e.g. "2024.3.2" -> [ y: 2024, m: 3, p: 2 ].
|
||||
const [ ay, am, ap ] = a.split('.').map(Number);
|
||||
const [ by, bm, bp ] = b.split('.').map(Number);
|
||||
return by - ay || bm - am || bp - ap;
|
||||
});
|
||||
|
||||
/**
|
||||
* @example Array<[version, jointPrefixCode]>
|
||||
* [
|
||||
* ... ..
|
||||
* [ '2024.2.2', 'L' ],
|
||||
* [ '2024.2.1', 'L' ],
|
||||
* [ '2024.1.3', 'L' ],
|
||||
* [ '2024.1.2', 'K' ],
|
||||
* [ '2024.1.1', 'K' ],
|
||||
* [ '2023.3.2', 'J|K' ],
|
||||
* [ '2023.3.1', 'J' ],
|
||||
* [ '2023.2.1', 'I' ],
|
||||
* ... ...
|
||||
* ]
|
||||
* @type {Array<[string, string]>}
|
||||
*/
|
||||
const versionLettersList = sortedVersions.map(v => [ v, Array.from(versionToLetters.get(v)).sort().join('|') ]);
|
||||
|
||||
/* e.g. { "2023.3": {1: "J", 2: "J|K"} }. */
|
||||
/** @type {Object<string, { [patch: number]: string }>} */
|
||||
/**
|
||||
* @example { minorSeries: { patch: letters } }
|
||||
* {
|
||||
* ... ...
|
||||
* '2025.1': { '1': 'N', '2': 'N', '3': 'N', '4': 'N' },
|
||||
* '2024.3': { '1': 'M', '2': 'M' },
|
||||
* '2024.2': { '1': 'L', '2': 'L' },
|
||||
* '2024.1': { '1': 'K', '2': 'K', '3': 'L' },
|
||||
* '2023.3': { '1': 'J', '2': 'J|K' },
|
||||
* '2023.2': { '1': 'I' },
|
||||
* '2023.1': { '1': 'H' },
|
||||
* '2022.3': { '1': 'G' },
|
||||
* ... ...
|
||||
* }
|
||||
* @type {Object<string, { [patch: number]: string }>}
|
||||
*/
|
||||
const rawVersionLettersMap = {};
|
||||
for (let i = 0; i < versionLettersList.length; i++) {
|
||||
const [ v, letters ] = versionLettersList[i];
|
||||
const matched = v.match(/(^\d+\.\d+)(?:\.(\d+))?/);
|
||||
if (!matched) continue;
|
||||
const [ , major, patch ] = matched;
|
||||
if (major in rawVersionLettersMap) {
|
||||
rawVersionLettersMap[major][patch] = letters;
|
||||
const [ , minorSeries, patch ] = matched;
|
||||
if (minorSeries in rawVersionLettersMap) {
|
||||
rawVersionLettersMap[minorSeries][patch] = letters;
|
||||
} else {
|
||||
rawVersionLettersMap[major] = { [patch]: letters };
|
||||
rawVersionLettersMap[minorSeries] = { [patch]: letters };
|
||||
}
|
||||
}
|
||||
|
||||
/* e.g. { "2024.3": "M", "2024.1.2": "K" }. */
|
||||
/**
|
||||
* When all versions with the same minor series prefix (like `2025.1.x`)
|
||||
* point to the same codename prefix (like `'N'`),
|
||||
* they can be merged (like `{ '2025.1' : 'N' }`, where `2025.1` is the minor series prefix),
|
||||
* otherwise retain the original split form (like `2024.1.x` cannot be merged).<br>
|
||||
* zh-CN:<br>
|
||||
* 当次版本系列相同的版本 (如 `2025.1.x`) 全部指向同一个代号前缀 (如 `'N'`) 时,
|
||||
* 可进行合并 (如 `{ '2025.1' : 'N' }`, 其中 `2025.1` 为次版本系列),
|
||||
* 否则保留原始的拆分形式 (如 `2024.1.x` 不可合并).
|
||||
* @example { version: letters }
|
||||
* {
|
||||
* ... ...
|
||||
* '2025.1': 'N',
|
||||
* '2024.3': 'M',
|
||||
* '2024.2': 'L',
|
||||
* '2024.1.1': 'K',
|
||||
* '2024.1.2': 'K',
|
||||
* '2024.1.3': 'L',
|
||||
* '2023.3.1': 'J',
|
||||
* '2023.3.2': 'J|K',
|
||||
* '2023.2': 'I',
|
||||
* '2023.1': 'H',
|
||||
* '2022.3': 'G',
|
||||
* ... ...
|
||||
* }
|
||||
* @type {Object<[version: string], string>}
|
||||
*/
|
||||
const combinedVersionLettersMap = {};
|
||||
|
||||
Object.entries(rawVersionLettersMap).forEach(([ major, patchToLetters ]) => {
|
||||
Object.entries(rawVersionLettersMap).forEach(([ minorSeries, patchToLetters ]) => {
|
||||
const letterValues = Object.values(patchToLetters);
|
||||
if (new Set(letterValues).size === 1) {
|
||||
combinedVersionLettersMap[major] = letterValues[0];
|
||||
/* Combine. (zh-CN: 合并.) */
|
||||
combinedVersionLettersMap[minorSeries] = letterValues[0];
|
||||
} else {
|
||||
/* Keep expanded. (zh-CN: 保持展开.) */
|
||||
Object.entries(patchToLetters).forEach(([ patch, letters ]) => {
|
||||
combinedVersionLettersMap[`${major}.${patch}`] = letters;
|
||||
combinedVersionLettersMap[`${minorSeries}.${patch}`] = letters;
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -288,32 +434,37 @@ async function main() {
|
||||
.sort((a, b) => compareVersionStrings(b[0], a[0]))
|
||||
.map(([ v, letters ]) => `"${v}" to "${letters}",`);
|
||||
|
||||
// 生成 codenameMap 文本 (按 born 日期倒序; 注释: Born on Mon d, yyyy.)
|
||||
// 这里 key 为自动生成的 code (可能为一到多字符), value 为完整代号
|
||||
const sortedLetters = Array.from(letterBorn.entries())
|
||||
.sort((a, b) => b[1].born.getTime() - a[1].born.getTime());
|
||||
|
||||
const codenameMapLines = sortedLetters.map(([ code, { name, born } ]) => {
|
||||
const bornStr = toUpdatedStamp(born);
|
||||
// e.g. `"M" to "Meerkat", /* Born on Nov 12, 2024. */`.
|
||||
return `"${code}" to "${name}", /* Born on ${bornStr}. */`;
|
||||
});
|
||||
|
||||
return { versionMapLines, codenameMapLines };
|
||||
}
|
||||
|
||||
(async function main() {
|
||||
const archives = await getAndroidStudioArchives();
|
||||
await updateLatestArchiveInfo(archives);
|
||||
|
||||
const { versionMapLines, codenameMapLines } = getCodenameMapLinesInfo(archives);
|
||||
await batchUpdateAnchoredBlocks('../settings.gradle.kts', [ {
|
||||
type: 'map',
|
||||
anchorTag: 'ANDROID_STUDIO_CODENAME_VERSION_MAP',
|
||||
mapName: 'codenameVersionMap',
|
||||
lines: versionMapLines,
|
||||
updatedLabel: 'Android Studio 代号版本映射',
|
||||
updatedLabel: 'Android Studio codename version map',
|
||||
}, {
|
||||
type: 'map',
|
||||
anchorTag: 'ANDROID_STUDIO_CODENAME_MAP',
|
||||
mapName: 'codenameMap',
|
||||
lines: codenameMapLines,
|
||||
updatedLabel: 'Android Studio 代号名称映射',
|
||||
updatedLabel: 'Android Studio codename map',
|
||||
} ]);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
})().catch(err => {
|
||||
console.error(err);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,7 @@ plugins {
|
||||
id("org.autojs.build.utils")
|
||||
id("org.autojs.build.versions")
|
||||
id("org.autojs.build.signs")
|
||||
id("org.autojs.build.jvm-convention")
|
||||
id("com.android.application")
|
||||
id("com.google.devtools.ksp")
|
||||
id("org.jetbrains.kotlin.android") /* kotlin("android") */
|
||||
@@ -567,8 +568,6 @@ android {
|
||||
|
||||
compileOptions {
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
sourceCompatibility = versions.javaVersion
|
||||
targetCompatibility = versions.javaVersion
|
||||
}
|
||||
|
||||
// @Hint by SuperMonster003 on Sep 25, 2024.
|
||||
@@ -619,12 +618,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
kotlinOptions {
|
||||
jvmTarget = versions.javaVersion.toString()
|
||||
// freeCompilerArgs = listOf("-Xjvm-default=all-compatibility")
|
||||
}
|
||||
|
||||
lint {
|
||||
abortOnError = false
|
||||
}
|
||||
@@ -745,12 +738,6 @@ tasks {
|
||||
// options.compilerArgs.addAll(listOf("-Xlint:deprecation", "-Xlint:unchecked"))
|
||||
}
|
||||
|
||||
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
|
||||
// @Archived by SuperMonster003 on Aug 14, 2024.
|
||||
// # kotlinOptions { jvmTarget = versions.javaVersion.toString() }
|
||||
compilerOptions { jvmTarget.set(JvmTarget.valueOf("JVM_${versions.javaVersion}")) }
|
||||
}
|
||||
|
||||
register<Copy>("appendDigestToReleasedFiles") {
|
||||
listOf(flavorNameApp, flavorNameInrt).forEach { flavorName ->
|
||||
val src = "$flavorName/$buildTypeRelease"
|
||||
|
||||
@@ -52,6 +52,14 @@ gradlePlugin {
|
||||
t.description = "Provides properties helpers."
|
||||
}
|
||||
})
|
||||
create("jvmConvention", object : Action<PluginDeclaration> {
|
||||
override fun execute(t: PluginDeclaration) {
|
||||
t.id = "org.autojs.build.jvm-convention"
|
||||
t.implementationClass = "org.autojs.build.JvmConventionPlugin"
|
||||
t.displayName = "AutoJs6 JVM Convention Plugin"
|
||||
t.description = "Configures Java/Kotlin targets for Android modules using central Versions."
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,3 +17,10 @@ dependencyResolutionManagement {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
plugins {
|
||||
// @Hint by SuperMonster003 on Sep 14, 2025.
|
||||
// ! Enable JDK auto-resolution/download capability for build modules.
|
||||
// ! zh-CN: 让构建模块具备 JDK 自动解析/下载能力.
|
||||
id("org.gradle.toolchains.foojay-resolver-convention")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
@file:Suppress("unused")
|
||||
|
||||
package org.autojs.build
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
|
||||
/**
|
||||
* Convention plugin: Unified Java/Kotlin target version configuration for Android modules.
|
||||
*
|
||||
* zh-CN: 约定插件: 为 Android 模块统一配置 Java/Kotlin 目标版本.
|
||||
*
|
||||
* - `id`: "org.autojs.build.jvm-convention"
|
||||
* - `implementationClass`: "org.autojs.build.JvmConventionPlugin"
|
||||
* - `displayName`: "AutoJs6 JVM Convention Plugin"
|
||||
* - `description`: "Configures Java/Kotlin targets for Android modules using central Versions."
|
||||
*
|
||||
* Apply this plugin to your Android module's `build.gradle.kts`:
|
||||
*
|
||||
* zh-CN: 在 Android 模块的 `build.gradle.kts` 中应用此插件:<br>
|
||||
*
|
||||
* ```kts
|
||||
* plugins {
|
||||
* id("org.autojs.build.jvm-convention")
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
class JvmConventionPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
Utils.configureJvmForAndroidModule(project)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,10 @@
|
||||
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
|
||||
# org.gradle.parallel=true
|
||||
#Thu Nov 11 16:38:18 CST 2021
|
||||
# Configure memory settings for Gradle daemon and Kotlin daemon
|
||||
# -Xms: Initial heap size
|
||||
# -Xmx: Maximum heap size
|
||||
# UseParallelGC: Use parallel garbage collector for better performance
|
||||
org.gradle.jvmargs=-Xms4g -Xmx4g -Dkotlin.daemon.jvm.options\="-Xmx4g" -Dfile.encoding\=UTF-8 -XX:+UseParallelGC
|
||||
# https://docs.gradle.org/current/userguide/gradle_daemon.html
|
||||
org.gradle.daemon=true
|
||||
@@ -35,7 +39,13 @@ android.enableJetifier=true
|
||||
# thereby reducing the size of the R class for that library
|
||||
android.nonTransitiveRClass=true
|
||||
android.nonFinalResIds=true
|
||||
# -- archived fields --
|
||||
# Added since Gradle Build Tool version 8.0.0-alpha09
|
||||
# Removed since Gradle Build Tool version 8.2.0-alpha15
|
||||
# Configure Java installation settings
|
||||
# auto-detect: Automatically detect Java installations on the system
|
||||
# auto-download: Automatically download required Java version if not found
|
||||
org.gradle.java.installations.auto-detect=true
|
||||
org.gradle.java.installations.auto-download=true
|
||||
# BuildConfig feature flag history:
|
||||
# - Added in Gradle Build Tool 8.0.0-alpha09
|
||||
# - Removed in Gradle Build Tool 8.2.0-alpha15
|
||||
# - Controls generation of BuildConfig class
|
||||
# android.defaults.buildfeatures.buildconfig=true
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
id 'org.autojs.build.utils'
|
||||
id 'org.autojs.build.properties'
|
||||
id 'org.autojs.build.jvm-convention'
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
@@ -68,15 +69,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = project.ext.javaVersion
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "CMakeLists.txt"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
plugins {
|
||||
id 'org.autojs.build.utils'
|
||||
id 'org.autojs.build.properties'
|
||||
id 'org.autojs.build.jvm-convention'
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
}
|
||||
@@ -125,15 +126,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = project.ext.javaVersion
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "src/main/cpp/CMakeLists.txt"
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
plugins {
|
||||
id 'org.autojs.build.utils'
|
||||
id 'org.autojs.build.properties'
|
||||
id 'org.autojs.build.jvm-convention'
|
||||
id 'com.android.library'
|
||||
id 'org.jetbrains.kotlin.android'
|
||||
id 'kotlin-parcelize'
|
||||
@@ -90,15 +91,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = project.ext.javaVersion
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "src/main/cpp/CMakeLists.txt"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
plugins {
|
||||
id 'org.autojs.build.versions'
|
||||
id 'org.autojs.build.jvm-convention'
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
@@ -17,15 +18,6 @@ android {
|
||||
consumerProguardFiles "consumer-rules.pro"
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = project.ext.javaVersion
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
plugins {
|
||||
id 'org.autojs.build.versions'
|
||||
id 'org.autojs.build.jvm-convention'
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
@@ -18,15 +19,6 @@ android {
|
||||
testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = project.ext.javaVersion
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs = ['src/main/java']
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
plugins {
|
||||
id 'org.autojs.build.versions'
|
||||
id 'org.autojs.build.jvm-convention'
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
@@ -18,15 +19,6 @@ android {
|
||||
versionName '1.1.0'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = project.ext.javaVersion
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
plugins {
|
||||
id 'org.autojs.build.versions'
|
||||
id 'org.autojs.build.jvm-convention'
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
@@ -19,15 +20,6 @@ android {
|
||||
testInstrumentationRunner = 'androidx.test.runner.AndroidJUnitRunner'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = project.ext.javaVersion
|
||||
}
|
||||
|
||||
sourceSets {
|
||||
main {
|
||||
// 主 Java/Kotlin 目录
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
plugins {
|
||||
id 'org.autojs.build.versions'
|
||||
id 'org.autojs.build.jvm-convention'
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
@@ -25,15 +26,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = project.ext.javaVersion
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
plugins {
|
||||
id 'org.autojs.build.versions'
|
||||
id 'org.autojs.build.jvm-convention'
|
||||
id 'com.android.library'
|
||||
id 'kotlin-android'
|
||||
}
|
||||
@@ -19,15 +20,6 @@ android {
|
||||
consumerProguardFiles 'progress-proguard.txt'
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
targetCompatibility = JavaVersion.toVersion(project.ext.javaVersion)
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = project.ext.javaVersion
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
@@ -509,6 +509,7 @@ pluginManagement {
|
||||
Classpath(id = "org.apache.commons:commons-compress", version = "toml:commons-compress"),
|
||||
Classpath(id = "org.tukaani:xz", version = "toml:xz"),
|
||||
Plugin(id = "com.google.devtools.ksp", version = overriddenKspVersion ?: "auto:ksp"),
|
||||
Plugin(id = "org.gradle.toolchains.foojay-resolver-convention", version = "toml:foojay-resolver-convention"),
|
||||
)
|
||||
|
||||
// @AnchorBegin KSP_VERSION_MAP
|
||||
@@ -987,3 +988,10 @@ pluginManagement {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
plugins {
|
||||
// @Hint by SuperMonster003 on Sep 14, 2025.
|
||||
// ! Enable JDK auto-resolution/download capability for build modules.
|
||||
// ! zh-CN: 让构建模块具备 JDK 自动解析/下载能力.
|
||||
id("org.gradle.toolchains.foojay-resolver-convention")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user