minecraft-dev-starter 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +166 -0
- package/README.md +124 -2
- package/cordis.patch.yml +7 -0
- package/lib/client.js +1521 -0
- package/package.json +54 -4
- package/src/client/Capsule.js +158 -0
- package/src/client/DetailTab.js +228 -0
- package/src/client/Flyout.js +191 -0
- package/src/client/LogTab.js +180 -0
- package/src/client/Skeleton.js +18 -0
- package/src/client/api.js +33 -0
- package/src/client/debug.js +27 -0
- package/src/client/format.js +89 -0
- package/src/client/h.js +8 -0
- package/src/client/icons.js +147 -0
- package/src/client/index.js +205 -0
- package/src/client/runtime.js +41 -0
- package/src/client/selection.js +154 -0
- package/src/client/state.js +233 -0
- package/src/client/store.js +34 -0
- package/src/client/styles.js +329 -0
- package/src/index.js +193 -0
- package/src/lib/api.js +186 -0
- package/src/lib/argv.js +66 -0
- package/src/lib/detect.js +460 -0
- package/src/lib/gradle.js +295 -0
- package/src/lib/java.js +204 -0
- package/src/lib/launches.js +495 -0
- package/src/lib/metadata.js +175 -0
- package/src/lib/scanner.js +131 -0
- package/src/lib/state.js +91 -0
- package/src/lib/text.js +92 -0
- package/src/lib/toml.js +281 -0
- package/src/tools.js +291 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gradle 构建脚本的静态解析。
|
|
3
|
+
*
|
|
4
|
+
* 只做"读得出事实"的解析,不求完整:
|
|
5
|
+
* - 注释/字符串安全的块提取(`runs { ... }` 的花括号匹配不能数错)
|
|
6
|
+
* - 插件 id、settings 的 include、run 配置、Java 等级、MC 版本字面量
|
|
7
|
+
*
|
|
8
|
+
* 识别引擎不执行 Gradle:本机实测 `gradlew tasks` 要 6 分半且可能失败,
|
|
9
|
+
* 见 doc/调研-DSH插件机制.md §8.1。
|
|
10
|
+
* @module minecraft-dev-starter/lib/gradle
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 去掉 Groovy/Kotlin 注释,字符串内容原样保留(替换成等长空白,索引不变)。
|
|
15
|
+
* @param {string} text
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function stripComments(text) {
|
|
19
|
+
let out = ''
|
|
20
|
+
let quote
|
|
21
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
22
|
+
const ch = text[i]
|
|
23
|
+
const next = text[i + 1]
|
|
24
|
+
|
|
25
|
+
if (quote !== undefined) {
|
|
26
|
+
out += ch
|
|
27
|
+
if (ch === '\\' && quote !== "'") {
|
|
28
|
+
out += next ?? ''
|
|
29
|
+
i += 1
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
if (ch === quote) quote = undefined
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (ch === '"' || ch === "'") {
|
|
37
|
+
quote = ch
|
|
38
|
+
out += ch
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (ch === '/' && next === '/') {
|
|
43
|
+
while (i < text.length && text[i] !== '\n') {
|
|
44
|
+
out += ' '
|
|
45
|
+
i += 1
|
|
46
|
+
}
|
|
47
|
+
out += '\n'
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (ch === '/' && next === '*') {
|
|
52
|
+
while (i < text.length && !(text[i] === '*' && text[i + 1] === '/')) {
|
|
53
|
+
out += text[i] === '\n' ? '\n' : ' '
|
|
54
|
+
i += 1
|
|
55
|
+
}
|
|
56
|
+
out += ' '
|
|
57
|
+
i += 1
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
out += ch
|
|
62
|
+
}
|
|
63
|
+
return out
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 提取 `name { ... }` 的块内容(花括号配平,已去注释)。
|
|
68
|
+
* @param {string} text 已去注释的脚本
|
|
69
|
+
* @param {string} name 块名,例如 `runs` / `loom`
|
|
70
|
+
* @returns {string[]}
|
|
71
|
+
*/
|
|
72
|
+
export function findBlocks(text, name) {
|
|
73
|
+
/** @type {string[]} */
|
|
74
|
+
const bodies = []
|
|
75
|
+
const pattern = new RegExp(`(^|[^\\w.])${name}\\s*\\{`, 'g')
|
|
76
|
+
let match
|
|
77
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
78
|
+
const open = text.indexOf('{', match.index + match[0].length - 1)
|
|
79
|
+
if (open < 0) break
|
|
80
|
+
const close = matchBrace(text, open)
|
|
81
|
+
if (close < 0) break
|
|
82
|
+
bodies.push(text.slice(open + 1, close))
|
|
83
|
+
pattern.lastIndex = close + 1
|
|
84
|
+
}
|
|
85
|
+
return bodies
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 从 `{` 的位置找配对的 `}`。
|
|
90
|
+
* @param {string} text
|
|
91
|
+
* @param {number} open
|
|
92
|
+
* @returns {number} 未配对时 -1
|
|
93
|
+
*/
|
|
94
|
+
function matchBrace(text, open) {
|
|
95
|
+
let depth = 0
|
|
96
|
+
for (let i = open; i < text.length; i += 1) {
|
|
97
|
+
if (text[i] === '{') depth += 1
|
|
98
|
+
else if (text[i] === '}') {
|
|
99
|
+
depth -= 1
|
|
100
|
+
if (depth === 0) return i
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return -1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 收集插件 id:`id 'x'` / `id("x")` / `apply plugin: 'x'`。
|
|
108
|
+
* @param {string} raw 原始脚本
|
|
109
|
+
* @returns {string[]}
|
|
110
|
+
*/
|
|
111
|
+
export function parsePluginIds(raw) {
|
|
112
|
+
const text = stripComments(raw)
|
|
113
|
+
/** @type {Set<string>} */
|
|
114
|
+
const ids = new Set()
|
|
115
|
+
for (const match of text.matchAll(/\bid\s*\(?\s*["']([\w.\-]+)["']/g)) ids.add(match[1])
|
|
116
|
+
for (const match of text.matchAll(/apply\s+plugin\s*:\s*["']([\w.\-]+)["']/g)) ids.add(match[1])
|
|
117
|
+
for (const match of text.matchAll(/kotlin\(\s*["']([\w.\-]+)["']\s*\)/g)) ids.add(match[1])
|
|
118
|
+
return [...ids]
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* settings.gradle(.kts) 里的 include 列表。
|
|
123
|
+
* @param {string} raw
|
|
124
|
+
* @returns {string[]} 形如 `fabric`、`common`(去掉前导冒号)
|
|
125
|
+
*/
|
|
126
|
+
export function parseSettingsIncludes(raw) {
|
|
127
|
+
const text = stripComments(raw)
|
|
128
|
+
/** @type {string[]} */
|
|
129
|
+
const modules = []
|
|
130
|
+
for (const match of text.matchAll(/include\s*\(([^)]*)\)/g)) {
|
|
131
|
+
pushIncludes(modules, match[1])
|
|
132
|
+
}
|
|
133
|
+
for (const match of text.matchAll(/include\s+((?:["'][^"']+["']\s*,?\s*)+)/g)) {
|
|
134
|
+
pushIncludes(modules, match[1])
|
|
135
|
+
}
|
|
136
|
+
return [...new Set(modules)]
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {string[]} modules
|
|
141
|
+
* @param {string} raw
|
|
142
|
+
*/
|
|
143
|
+
function pushIncludes(modules, raw) {
|
|
144
|
+
for (const match of raw.matchAll(/["']([^"']+)["']/g)) {
|
|
145
|
+
const name = match[1].trim().replace(/^:/, '')
|
|
146
|
+
if (name !== '') modules.push(name)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* @typedef {object} RunTarget
|
|
152
|
+
* @property {string} block runs 块名,如 `client`
|
|
153
|
+
* @property {string} kind 块里调用的类型,如 `client`
|
|
154
|
+
* @property {string} task Gradle 任务名,如 `runClient`
|
|
155
|
+
* @property {string} label 界面上显示的名字,如 `Client`
|
|
156
|
+
*/
|
|
157
|
+
|
|
158
|
+
const LABELS = {
|
|
159
|
+
client: 'Client',
|
|
160
|
+
server: 'Server',
|
|
161
|
+
data: 'Data',
|
|
162
|
+
gameTestServer: 'GameTestServer',
|
|
163
|
+
gameTest: 'GameTestServer',
|
|
164
|
+
datagen: 'Data',
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 解析 `runs { client { client() } ... }`,得到可运行目标。
|
|
169
|
+
* 任务名规则:`runs` 块名首字母大写后拼到 `run` 后面(Gradle 的 RunConfig 约定)。
|
|
170
|
+
* @param {string} raw
|
|
171
|
+
* @returns {RunTarget[]}
|
|
172
|
+
*/
|
|
173
|
+
export function parseRuns(raw) {
|
|
174
|
+
const text = stripComments(raw)
|
|
175
|
+
/** @type {RunTarget[]} */
|
|
176
|
+
const targets = []
|
|
177
|
+
for (const body of findBlocks(text, 'runs')) {
|
|
178
|
+
for (const match of body.matchAll(/(^|[^\w.])("?)([A-Za-z_][\w]*)\2\s*\{/g)) {
|
|
179
|
+
const name = match[3]
|
|
180
|
+
const open = body.indexOf('{', match.index + match[0].length - 1)
|
|
181
|
+
if (open < 0) continue
|
|
182
|
+
const close = matchBrace(body, open)
|
|
183
|
+
if (close < 0) continue
|
|
184
|
+
const inner = body.slice(open + 1, close)
|
|
185
|
+
const call = /(^|[^\w.])([A-Za-z_][\w]*)\s*\(/.exec(inner)
|
|
186
|
+
const kind = call?.[2] ?? name
|
|
187
|
+
targets.push({
|
|
188
|
+
block: name,
|
|
189
|
+
kind,
|
|
190
|
+
task: `run${name[0].toUpperCase()}${name.slice(1)}`,
|
|
191
|
+
label: LABELS[kind] ?? LABELS[name] ?? `${kind}`,
|
|
192
|
+
})
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
// 同名任务去重(多模块时由调用方按模块分组)
|
|
196
|
+
const seen = new Set()
|
|
197
|
+
return targets.filter((target) => {
|
|
198
|
+
if (seen.has(target.task)) return false
|
|
199
|
+
seen.add(target.task)
|
|
200
|
+
return true
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Java toolchain / 兼容等级。
|
|
206
|
+
* @param {string} raw
|
|
207
|
+
* @returns {number | undefined}
|
|
208
|
+
*/
|
|
209
|
+
export function parseJavaLevel(raw) {
|
|
210
|
+
const text = stripComments(raw)
|
|
211
|
+
const patterns = [
|
|
212
|
+
/JavaLanguageVersion\.of\(\s*(\d+)\s*\)/,
|
|
213
|
+
/jvmToolchain\(\s*(\d+)\s*\)/,
|
|
214
|
+
/sourceCompatibility\s*=\s*(?:JavaVersion\.VERSION_)?(\d+)/,
|
|
215
|
+
/targetCompatibility\s*=\s*(?:JavaVersion\.VERSION_)?(\d+)/,
|
|
216
|
+
/languageVersion\s*=\s*JavaLanguageVersion\.of\(\s*(\d+)\s*\)/,
|
|
217
|
+
]
|
|
218
|
+
for (const pattern of patterns) {
|
|
219
|
+
const match = pattern.exec(text)
|
|
220
|
+
if (match) return Number(match[1])
|
|
221
|
+
}
|
|
222
|
+
return undefined
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* 从构建脚本里找 MC 版本字面量。
|
|
227
|
+
* @param {string} raw
|
|
228
|
+
* @param {Record<string, string>} properties
|
|
229
|
+
* @returns {string | undefined}
|
|
230
|
+
*/
|
|
231
|
+
export function parseMinecraftVersion(raw, properties) {
|
|
232
|
+
const text = stripComments(raw)
|
|
233
|
+
|
|
234
|
+
// loom: minecraft "com.mojang:minecraft:1.20.1"
|
|
235
|
+
const dependency = /minecraft\s*\(?\s*["'][^"']*minecraft:(\d+\.\d+(?:\.\d+)?)["']/.exec(text)
|
|
236
|
+
if (dependency) return dependency[1]
|
|
237
|
+
|
|
238
|
+
// forge: minecraft { version = "1.20.1-47.2.0" }
|
|
239
|
+
const forgeBlock = /minecraft\s*\{[^}]*?version\s*=\s*["'](\d+\.\d+(?:\.\d+)?)/.exec(text)
|
|
240
|
+
if (forgeBlock) return forgeBlock[1]
|
|
241
|
+
|
|
242
|
+
// 直接写死:minecraft_version = "1.21.1" / minecraftVersion = '1.21.1'
|
|
243
|
+
const literal = /(?:minecraft_version|minecraftVersion|mc_version|mcVersion)\s*[=:]\s*["']?(\d+\.\d+(?:\.\d+)?)["']?/.exec(
|
|
244
|
+
`${text}\n${renderProperties(properties)}`,
|
|
245
|
+
)
|
|
246
|
+
if (literal) return literal[1]
|
|
247
|
+
|
|
248
|
+
return undefined
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* @param {Record<string, string>} properties
|
|
253
|
+
* @returns {string}
|
|
254
|
+
*/
|
|
255
|
+
function renderProperties(properties) {
|
|
256
|
+
return Object.entries(properties)
|
|
257
|
+
.map(([key, value]) => `${key}=${value}`)
|
|
258
|
+
.join('\n')
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* wrapper 里的 Gradle 版本。
|
|
263
|
+
* @param {Record<string, string>} wrapperProperties
|
|
264
|
+
* @returns {string | undefined}
|
|
265
|
+
*/
|
|
266
|
+
export function parseWrapperVersion(wrapperProperties) {
|
|
267
|
+
const url = wrapperProperties.distributionUrl
|
|
268
|
+
if (!url) return undefined
|
|
269
|
+
const match = /gradle-(\d+\.\d+(?:\.\d+)?)-(?:bin|all)\.zip/.exec(url)
|
|
270
|
+
return match?.[1]
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* NeoForge 版本 → MC 版本。`21.1.251 → 1.21.1`、`20.4.237 → 1.20.4`、`20.2.x → 1.20.2`。
|
|
275
|
+
* 只映射到小版本(1.20.x 线),映射不到返回 undefined,不猜。
|
|
276
|
+
* @param {string | undefined} neoVersion
|
|
277
|
+
* @returns {string | undefined}
|
|
278
|
+
*/
|
|
279
|
+
export function minecraftFromNeoVersion(neoVersion) {
|
|
280
|
+
if (!neoVersion) return undefined
|
|
281
|
+
const match = /^(\d+)\.(\d+)\./.exec(neoVersion.trim())
|
|
282
|
+
if (!match) return undefined
|
|
283
|
+
const major = Number(match[1])
|
|
284
|
+
const minor = Number(match[2])
|
|
285
|
+
if (major === 20) {
|
|
286
|
+
// 20.2 → 1.20.2、20.4 → 1.20.4、20.6 → 1.20.6
|
|
287
|
+
if (minor >= 2) return `1.20.${minor}`
|
|
288
|
+
return '1.20.1'
|
|
289
|
+
}
|
|
290
|
+
if (major === 21 || major === 22 || major === 23 || major >= 24) {
|
|
291
|
+
// 21.1 → 1.21.1;未来大版本按同样规律,但只在小版本 >0 时给三位
|
|
292
|
+
return minor > 0 ? `1.${major}.${minor}` : `1.${major}`
|
|
293
|
+
}
|
|
294
|
+
return undefined
|
|
295
|
+
}
|
package/src/lib/java.js
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JDK 需求推导与探测。
|
|
3
|
+
*
|
|
4
|
+
* 只读文件系统,不启动 `java -version`:识别面板要在打开瞬间出结果,
|
|
5
|
+
* 起进程既慢又会被 PATH 上的无关 JDK 带偏。版本从 JDK 目录里的 `release` 文件读。
|
|
6
|
+
* @module minecraft-dev-starter/lib/java
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { readdir } from 'node:fs/promises'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import { readText } from './text.js'
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 目标 MC 版本需要的 JDK 大版本。
|
|
15
|
+
* 1.7.10/1.12.2/1.16.5 → 8;1.17 → 16;1.18–1.20.4 → 17;1.20.5+/1.21+ → 21;26.x → 25。
|
|
16
|
+
* @param {string | undefined} mcVersion
|
|
17
|
+
* @returns {number | undefined}
|
|
18
|
+
*/
|
|
19
|
+
export function requiredJava(mcVersion) {
|
|
20
|
+
if (mcVersion === undefined) return undefined
|
|
21
|
+
const parts = mcVersion.split('.').map((part) => Number(part))
|
|
22
|
+
if (parts.some((part) => Number.isNaN(part))) return undefined
|
|
23
|
+
const [major, minor = 0, patch = 0] = parts
|
|
24
|
+
if (major >= 26) return 25
|
|
25
|
+
if (major !== 1) return undefined
|
|
26
|
+
if (minor >= 21) return 21
|
|
27
|
+
if (minor === 20) return patch >= 5 ? 21 : 17
|
|
28
|
+
if (minor >= 18) return 17
|
|
29
|
+
if (minor === 17) return 16
|
|
30
|
+
return 8
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Gradle 从哪个版本开始支持某个 JDK 大版本。
|
|
35
|
+
* 只列已知边界;不在表里的按 0 处理。
|
|
36
|
+
* @type {Record<number, string>}
|
|
37
|
+
*/
|
|
38
|
+
const GRADLE_JDK_FLOOR = {
|
|
39
|
+
25: '9.0',
|
|
40
|
+
24: '8.14',
|
|
41
|
+
23: '8.10',
|
|
42
|
+
22: '8.8',
|
|
43
|
+
21: '8.5',
|
|
44
|
+
20: '8.3',
|
|
45
|
+
19: '7.6',
|
|
46
|
+
17: '7.3',
|
|
47
|
+
16: '7.0',
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 判断 wrapper 的 Gradle 能不能跑这个 JDK。
|
|
52
|
+
* @param {string | undefined} gradleVersion
|
|
53
|
+
* @param {number | undefined} javaMajor
|
|
54
|
+
* @returns {{ ok: boolean, reason?: string }}
|
|
55
|
+
*/
|
|
56
|
+
export function gradleSupportsJava(gradleVersion, javaMajor) {
|
|
57
|
+
if (gradleVersion === undefined || javaMajor === undefined) return { ok: true }
|
|
58
|
+
const floor = GRADLE_JDK_FLOOR[javaMajor]
|
|
59
|
+
if (floor === undefined) return { ok: true }
|
|
60
|
+
if (compareVersions(gradleVersion, floor) >= 0) return { ok: true }
|
|
61
|
+
return {
|
|
62
|
+
ok: false,
|
|
63
|
+
reason: `wrapper 是 Gradle ${gradleVersion},JDK ${javaMajor} 需要 Gradle ${floor} 以上`,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string} a
|
|
69
|
+
* @param {string} b
|
|
70
|
+
* @returns {number}
|
|
71
|
+
*/
|
|
72
|
+
export function compareVersions(a, b) {
|
|
73
|
+
const left = a.split('.').map((part) => Number(part) || 0)
|
|
74
|
+
const right = b.split('.').map((part) => Number(part) || 0)
|
|
75
|
+
const length = Math.max(left.length, right.length)
|
|
76
|
+
for (let i = 0; i < length; i += 1) {
|
|
77
|
+
const diff = (left[i] ?? 0) - (right[i] ?? 0)
|
|
78
|
+
if (diff !== 0) return diff
|
|
79
|
+
}
|
|
80
|
+
return 0
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* @typedef {object} JdkInfo
|
|
85
|
+
* @property {string} home JDK 根目录
|
|
86
|
+
* @property {string} version 完整版本号,如 21.0.7
|
|
87
|
+
* @property {number} major 大版本,如 21
|
|
88
|
+
* @property {string} source 从哪找到的:env / dir:xxx
|
|
89
|
+
*/
|
|
90
|
+
|
|
91
|
+
/** 常见的 JDK 安装根目录(Windows 为主,其他平台给几个通用位置)。 */
|
|
92
|
+
function jdkRoots(env = process.env, platform = process.platform) {
|
|
93
|
+
/** @type {string[]} */
|
|
94
|
+
const roots = []
|
|
95
|
+
if (env.JAVA_HOME) roots.push(env.JAVA_HOME)
|
|
96
|
+
if (env.MC_DEV_STARTER_JDKS) {
|
|
97
|
+
for (const entry of env.MC_DEV_STARTER_JDKS.split(';')) {
|
|
98
|
+
if (entry.trim() !== '') roots.push(entry.trim())
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (platform === 'win32') {
|
|
102
|
+
roots.push(
|
|
103
|
+
'C:\\Program Files\\Java',
|
|
104
|
+
'C:\\Program Files\\Microsoft',
|
|
105
|
+
'C:\\Program Files\\Eclipse Adoptium',
|
|
106
|
+
'C:\\Program Files\\BellSoft',
|
|
107
|
+
'C:\\Program Files\\Zulu',
|
|
108
|
+
'C:\\Program Files\\Amazon Corretto',
|
|
109
|
+
'C:\\Program Files (x86)\\Java',
|
|
110
|
+
)
|
|
111
|
+
} else {
|
|
112
|
+
roots.push('/usr/lib/jvm', '/Library/Java/JavaVirtualMachines', join(env.HOME ?? '', '.sdkman/candidates/java'))
|
|
113
|
+
}
|
|
114
|
+
if (env.USERPROFILE) roots.push(join(env.USERPROFILE, '.jdks'))
|
|
115
|
+
return roots
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* 找本机所有 JDK。
|
|
120
|
+
* @param {{ env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform, extraDirs?: string[] }} [options]
|
|
121
|
+
* @returns {Promise<JdkInfo[]>}
|
|
122
|
+
*/
|
|
123
|
+
export async function discoverJdks(options = {}) {
|
|
124
|
+
const env = options.env ?? process.env
|
|
125
|
+
const platform = options.platform ?? process.platform
|
|
126
|
+
/** @type {Set<string>} */
|
|
127
|
+
const candidates = new Set()
|
|
128
|
+
for (const dir of options.extraDirs ?? []) candidates.add(dir)
|
|
129
|
+
for (const entry of jdkRoots(env, platform)) {
|
|
130
|
+
if (!entry) continue
|
|
131
|
+
candidates.add(entry)
|
|
132
|
+
// 根目录本身可能就是 JDK(JAVA_HOME / .jdks/<name>),也可能是装了一堆 JDK 的父目录
|
|
133
|
+
for (const child of await safeReaddir(entry)) {
|
|
134
|
+
candidates.add(join(entry, child))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** @type {Map<string, JdkInfo>} */
|
|
139
|
+
const found = new Map()
|
|
140
|
+
for (const home of candidates) {
|
|
141
|
+
const info = await readJdk(home)
|
|
142
|
+
if (info === undefined) continue
|
|
143
|
+
if (!found.has(info.home)) found.set(info.home, info)
|
|
144
|
+
}
|
|
145
|
+
return [...found.values()].sort((left, right) => right.major - left.major)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* @param {string} dir
|
|
150
|
+
* @returns {Promise<string[]>}
|
|
151
|
+
*/
|
|
152
|
+
async function safeReaddir(dir) {
|
|
153
|
+
try {
|
|
154
|
+
return await readdir(dir)
|
|
155
|
+
} catch {
|
|
156
|
+
return []
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* 读一个目录像不像 JDK,并拿到版本。
|
|
162
|
+
* @param {string} home
|
|
163
|
+
* @returns {Promise<JdkInfo | undefined>}
|
|
164
|
+
*/
|
|
165
|
+
async function readJdk(home) {
|
|
166
|
+
const release = await readText(join(home, 'release'))
|
|
167
|
+
if (release !== undefined) {
|
|
168
|
+
const match = /JAVA_VERSION="([^"]+)"/.exec(release)
|
|
169
|
+
if (match) {
|
|
170
|
+
const version = match[1]
|
|
171
|
+
const major = Number.parseInt(version, 10)
|
|
172
|
+
if (!Number.isNaN(major)) return { home, version, major, source: 'release' }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// 没有 release(比如某些精简发行版)时退一步看目录名
|
|
176
|
+
const name = home.split(/[\\/]/).filter(Boolean).at(-1) ?? ''
|
|
177
|
+
const byName = /jdk-?(\d+)(?:\.\d+)*/.exec(name) ?? /java-?1\.(\d+)/.exec(name)
|
|
178
|
+
if (byName) {
|
|
179
|
+
const major = Number(byName[1])
|
|
180
|
+
if (!Number.isNaN(major)) return { home, version: String(major), major, source: 'dirname' }
|
|
181
|
+
}
|
|
182
|
+
return undefined
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 挑一个满足要求的 JDK:优先大版本完全匹配,其次取不低于要求的最高版本。
|
|
187
|
+
* @param {JdkInfo[]} jdks
|
|
188
|
+
* @param {number | undefined} required
|
|
189
|
+
* @param {string | undefined} configured 显式配置的 JAVA_HOME / org.gradle.java.home
|
|
190
|
+
* @returns {{ home: string, exact: boolean } | undefined}
|
|
191
|
+
*/
|
|
192
|
+
export function pickJdk(jdks, required, configured) {
|
|
193
|
+
if (configured) {
|
|
194
|
+
const hit = jdks.find((jdk) => jdk.home.toLowerCase() === configured.toLowerCase())
|
|
195
|
+
if (hit) return { home: hit.home, exact: hit.major === required }
|
|
196
|
+
return { home: configured, exact: false }
|
|
197
|
+
}
|
|
198
|
+
if (required === undefined) return jdks[0] ? { home: jdks[0].home, exact: false } : undefined
|
|
199
|
+
const exact = jdks.find((jdk) => jdk.major === required)
|
|
200
|
+
if (exact) return { home: exact.home, exact: true }
|
|
201
|
+
const above = jdks.find((jdk) => jdk.major > required)
|
|
202
|
+
if (above) return { home: above.home, exact: false }
|
|
203
|
+
return undefined
|
|
204
|
+
}
|