@redpanda-data/docs-extensions-and-macros 5.2.5 → 5.3.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/bin/doc-tools.js +159 -0
- package/package.json +1 -1
- package/tools/property-extractor/property_extractor.py +39 -2
- package/tools/property-extractor/transformers.py +29 -13
- package/tools/rpk-docs/detect-platform-commands.js +566 -0
- package/tools/rpk-docs/generate-plugin-stubs.js +291 -0
- package/tools/rpk-docs/generate-rpk-docs.js +466 -30
- package/tools/rpk-docs/helpers/index.js +13 -3
- package/tools/rpk-docs/report-delta.js +314 -32
- package/tools/rpk-docs/rpk-docs-handler.js +1329 -208
- package/tools/rpk-docs/templates/command.hbs +17 -0
- package/tools/rpk-docs/templates/content-item.hbs +2 -2
- package/tools/rpk-docs/validate-overrides.js +16 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Static (source-based) detection of Linux-only rpk commands.
|
|
5
|
+
*
|
|
6
|
+
* Works on any platform (including Linux CI runners, where the dynamic
|
|
7
|
+
* "build on Linux and Darwin and diff the trees" approach is impossible
|
|
8
|
+
* because a cross-compiled darwin rpk cannot be executed).
|
|
9
|
+
*
|
|
10
|
+
* How it works
|
|
11
|
+
* ------------
|
|
12
|
+
* rpk gates platform-specific commands with Go build constraints:
|
|
13
|
+
*
|
|
14
|
+
* 1. Whole-package gating: every file in a command's package carries a
|
|
15
|
+
* Linux constraint (explicit `//go:build linux` tag or a `_linux.go`
|
|
16
|
+
* filename suffix) and no darwin-buildable counterpart exists.
|
|
17
|
+
* Example: pkg/cli/iotune, pkg/cli/redpanda/tune.
|
|
18
|
+
*
|
|
19
|
+
* 2. Dual registration: a package provides two variants of the same
|
|
20
|
+
* command constructor, one per platform, registering different
|
|
21
|
+
* subcommand sets. Example: pkg/cli/redpanda/redpanda.go
|
|
22
|
+
* (`//go:build linux`, registers start/stop/check/mode/config/tune)
|
|
23
|
+
* vs pkg/cli/redpanda/redpanda_darwin.go (`//go:build darwin`,
|
|
24
|
+
* registers only the hidden admin command). Same pattern at the root:
|
|
25
|
+
* pkg/cli/root_linux.go vs pkg/cli/root_darwin.go.
|
|
26
|
+
*
|
|
27
|
+
* This module detects both patterns:
|
|
28
|
+
*
|
|
29
|
+
* - For every scanned package, it computes which files build on linux
|
|
30
|
+
* and which build on darwin (from `//go:build` / `// +build` tags AND
|
|
31
|
+
* filename-implied constraints like `_linux.go`).
|
|
32
|
+
* - Packages whose files all fail to build on darwin (but build on
|
|
33
|
+
* linux) and that define a cobra command constructor are Linux-only.
|
|
34
|
+
* - Packages containing platform-differential files get a registration
|
|
35
|
+
* diff: subcommand constructors referenced from linux-buildable files
|
|
36
|
+
* but not from darwin-buildable files are Linux-only. Deprecated and
|
|
37
|
+
* hidden constructors are ignored because `rpk --print-tree` (the
|
|
38
|
+
* basis of the generated docs) excludes hidden commands.
|
|
39
|
+
*
|
|
40
|
+
* Scope: built-in commands only. Plugin commands (rpk connect, rpk ai, ...)
|
|
41
|
+
* are not part of the rpk source tree, so their platform availability
|
|
42
|
+
* cannot be derived here. Plugins keep whatever platform behavior the
|
|
43
|
+
* caller's other detection paths provide (for example, dynamic detection
|
|
44
|
+
* marks plugin-only commands when plugins are only installed in the Linux
|
|
45
|
+
* build container).
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
const fs = require('fs')
|
|
49
|
+
const path = require('path')
|
|
50
|
+
|
|
51
|
+
/** GOOS values recognized in filename suffixes and build tags */
|
|
52
|
+
const KNOWN_GOOS = new Set([
|
|
53
|
+
'aix', 'android', 'darwin', 'dragonfly', 'freebsd', 'hurd', 'illumos',
|
|
54
|
+
'ios', 'js', 'linux', 'nacl', 'netbsd', 'openbsd', 'plan9', 'solaris',
|
|
55
|
+
'wasip1', 'windows', 'zos'
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
/** GOOS values that satisfy the `unix` build tag */
|
|
59
|
+
const UNIX_GOOS = new Set([
|
|
60
|
+
'aix', 'android', 'darwin', 'dragonfly', 'freebsd', 'hurd', 'illumos',
|
|
61
|
+
'ios', 'linux', 'netbsd', 'openbsd', 'solaris'
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
/** GOARCH values recognized in filename suffixes */
|
|
65
|
+
const KNOWN_GOARCH = new Set([
|
|
66
|
+
'386', 'amd64', 'amd64p32', 'arm', 'arm64', 'arm64be', 'armbe', 'loong64',
|
|
67
|
+
'mips', 'mips64', 'mips64le', 'mips64p32', 'mips64p32le', 'mipsle',
|
|
68
|
+
'ppc', 'ppc64', 'ppc64le', 'riscv', 'riscv64', 's390', 's390x',
|
|
69
|
+
'sparc', 'sparc64', 'wasm'
|
|
70
|
+
])
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Directories (relative to the rpk source root) scanned for command
|
|
74
|
+
* implementations. Modern rpk keeps commands in pkg/cli/<command>/;
|
|
75
|
+
* the other roots cover older source layouts.
|
|
76
|
+
*/
|
|
77
|
+
const SCAN_ROOTS = ['pkg/cli', 'pkg/cli/cmd', 'cmd/rpk']
|
|
78
|
+
|
|
79
|
+
/** Directory names that never correspond to a command path segment */
|
|
80
|
+
const NON_COMMAND_SEGMENTS = new Set(['internal', 'common', 'testdata'])
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Determine the GOOS constraint implied by a Go filename.
|
|
84
|
+
* Follows Go's rules: *_GOOS.go, *_GOOS_GOARCH.go constrain the OS;
|
|
85
|
+
* *_GOARCH.go alone does not.
|
|
86
|
+
* @param {string} fileName - e.g. 'redpanda_darwin.go', 'bundle_k8s_linux.go'
|
|
87
|
+
* @returns {string|null} GOOS name or null when unconstrained
|
|
88
|
+
*/
|
|
89
|
+
function goosFromFileName(fileName) {
|
|
90
|
+
const base = fileName.replace(/\.go$/, '')
|
|
91
|
+
const parts = base.split('_')
|
|
92
|
+
if (parts.length < 2) return null
|
|
93
|
+
|
|
94
|
+
const last = parts[parts.length - 1]
|
|
95
|
+
if (KNOWN_GOOS.has(last)) return last
|
|
96
|
+
if (KNOWN_GOARCH.has(last) && parts.length >= 3) {
|
|
97
|
+
const secondLast = parts[parts.length - 2]
|
|
98
|
+
if (KNOWN_GOOS.has(secondLast)) return secondLast
|
|
99
|
+
}
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Evaluate a `//go:build` constraint expression for a target GOOS.
|
|
105
|
+
* Supports identifiers, !, &&, || and parentheses.
|
|
106
|
+
* Unknown identifiers (custom build tags) evaluate to false, matching a
|
|
107
|
+
* default `go build` with no -tags flag. GOARCH identifiers evaluate to
|
|
108
|
+
* true because we only care about OS-level availability.
|
|
109
|
+
* @param {string} expr - Expression after `//go:build`
|
|
110
|
+
* @param {string} goos - Target GOOS ('linux' or 'darwin')
|
|
111
|
+
* @returns {boolean}
|
|
112
|
+
*/
|
|
113
|
+
function evaluateBuildExpr(expr, goos) {
|
|
114
|
+
const tokens = expr.match(/[A-Za-z0-9_.]+|&&|\|\||!|\(|\)/g) || []
|
|
115
|
+
let pos = 0
|
|
116
|
+
|
|
117
|
+
const peek = () => tokens[pos]
|
|
118
|
+
const next = () => tokens[pos++]
|
|
119
|
+
|
|
120
|
+
function evalIdent(ident) {
|
|
121
|
+
if (KNOWN_GOOS.has(ident)) return ident === goos
|
|
122
|
+
if (ident === 'unix') return UNIX_GOOS.has(goos)
|
|
123
|
+
if (KNOWN_GOARCH.has(ident)) return true
|
|
124
|
+
if (ident === 'cgo') return true
|
|
125
|
+
// Custom build tags (e.g. withasan, integration) are unset by default
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parsePrimary() {
|
|
130
|
+
const tok = next()
|
|
131
|
+
if (tok === '(') {
|
|
132
|
+
const val = parseOr()
|
|
133
|
+
if (peek() === ')') next()
|
|
134
|
+
return val
|
|
135
|
+
}
|
|
136
|
+
if (tok === '!') return !parsePrimary()
|
|
137
|
+
if (tok === undefined) return false
|
|
138
|
+
return evalIdent(tok)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function parseAnd() {
|
|
142
|
+
let val = parsePrimary()
|
|
143
|
+
while (peek() === '&&') {
|
|
144
|
+
next()
|
|
145
|
+
val = parsePrimary() && val
|
|
146
|
+
}
|
|
147
|
+
return val
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseOr() {
|
|
151
|
+
let val = parseAnd()
|
|
152
|
+
while (peek() === '||') {
|
|
153
|
+
next()
|
|
154
|
+
val = parseAnd() || val
|
|
155
|
+
}
|
|
156
|
+
return val
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return parseOr()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Evaluate legacy `// +build` lines for a target GOOS.
|
|
164
|
+
* Multiple lines AND together; within a line, space-separated options OR
|
|
165
|
+
* and comma-separated terms AND.
|
|
166
|
+
* @param {string[]} lines - Contents after `// +build` (one per line)
|
|
167
|
+
* @param {string} goos - Target GOOS
|
|
168
|
+
* @returns {boolean}
|
|
169
|
+
*/
|
|
170
|
+
function evaluatePlusBuildLines(lines, goos) {
|
|
171
|
+
const evalTerm = (term) => {
|
|
172
|
+
let negate = false
|
|
173
|
+
while (term.startsWith('!')) {
|
|
174
|
+
negate = !negate
|
|
175
|
+
term = term.slice(1)
|
|
176
|
+
}
|
|
177
|
+
let val
|
|
178
|
+
if (KNOWN_GOOS.has(term)) val = term === goos
|
|
179
|
+
else if (term === 'unix') val = UNIX_GOOS.has(goos)
|
|
180
|
+
else if (KNOWN_GOARCH.has(term) || term === 'cgo') val = true
|
|
181
|
+
else val = false
|
|
182
|
+
return negate ? !val : val
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return lines.every(line => {
|
|
186
|
+
const options = line.trim().split(/\s+/).filter(Boolean)
|
|
187
|
+
if (options.length === 0) return true
|
|
188
|
+
return options.some(opt => opt.split(',').every(evalTerm))
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Extract the build constraint (if any) from a Go file's header.
|
|
194
|
+
* Only lines before the `package` declaration are considered.
|
|
195
|
+
* @param {string} content - Go file content
|
|
196
|
+
* @returns {{goBuildExpr: string|null, plusBuildLines: string[]}}
|
|
197
|
+
*/
|
|
198
|
+
function extractBuildConstraint(content) {
|
|
199
|
+
const header = content.split(/^package\s/m)[0]
|
|
200
|
+
const goBuildMatch = header.match(/^\/\/go:build\s+(.+)$/m)
|
|
201
|
+
const plusBuildLines = []
|
|
202
|
+
const plusBuildRe = /^\/\/\s*\+build\s+(.+)$/gm
|
|
203
|
+
let m
|
|
204
|
+
while ((m = plusBuildRe.exec(header)) !== null) {
|
|
205
|
+
plusBuildLines.push(m[1])
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
goBuildExpr: goBuildMatch ? goBuildMatch[1].trim() : null,
|
|
209
|
+
plusBuildLines
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Determine whether a Go file builds for a target GOOS, taking both the
|
|
215
|
+
* filename-implied constraint and explicit build tags into account.
|
|
216
|
+
* @param {string} fileName - Base name of the file
|
|
217
|
+
* @param {string} content - File content
|
|
218
|
+
* @param {string} goos - Target GOOS ('linux' or 'darwin')
|
|
219
|
+
* @returns {boolean}
|
|
220
|
+
*/
|
|
221
|
+
function fileBuildsOn(fileName, content, goos) {
|
|
222
|
+
const fileGoos = goosFromFileName(fileName)
|
|
223
|
+
if (fileGoos && fileGoos !== goos) return false
|
|
224
|
+
|
|
225
|
+
const { goBuildExpr, plusBuildLines } = extractBuildConstraint(content)
|
|
226
|
+
if (goBuildExpr) return evaluateBuildExpr(goBuildExpr, goos)
|
|
227
|
+
if (plusBuildLines.length > 0) return evaluatePlusBuildLines(plusBuildLines, goos)
|
|
228
|
+
return true
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Derive a command name from a cobra constructor function name.
|
|
233
|
+
* NewStartCommand -> start, newTuneCommand -> tune, NewCommand -> null.
|
|
234
|
+
* @param {string} funcName
|
|
235
|
+
* @returns {string|null}
|
|
236
|
+
*/
|
|
237
|
+
function commandNameFromConstructor(funcName) {
|
|
238
|
+
const m = funcName.match(/^(?:New|new)(\w+?)(?:Command|Cmd)$/)
|
|
239
|
+
if (m && m[1]) return m[1].toLowerCase()
|
|
240
|
+
return null
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Parse cobra command constructors defined in a Go file.
|
|
245
|
+
* A constructor is any `func XxxYyy(...) *cobra.Command`. For each one we
|
|
246
|
+
* record the cobra `Use:` name (first token) and whether the command is
|
|
247
|
+
* deprecated or hidden (such commands never appear in `rpk --print-tree`).
|
|
248
|
+
* @param {string} content - Go file content
|
|
249
|
+
* @returns {Object<string, {useName: string|null, excluded: boolean}>}
|
|
250
|
+
*/
|
|
251
|
+
function parseConstructors(content) {
|
|
252
|
+
const constructors = {}
|
|
253
|
+
const funcRe = /^func\s+(\w+)\s*\([^)]*\)\s*\*cobra\.Command\s*\{/gm
|
|
254
|
+
const matches = []
|
|
255
|
+
let m
|
|
256
|
+
while ((m = funcRe.exec(content)) !== null) {
|
|
257
|
+
matches.push({ name: m[1], start: m.index })
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (let i = 0; i < matches.length; i++) {
|
|
261
|
+
const bodyEnd = i + 1 < matches.length ? matches[i + 1].start : content.length
|
|
262
|
+
const body = content.slice(matches[i].start, bodyEnd)
|
|
263
|
+
|
|
264
|
+
const useMatch = body.match(/\bUse:\s*"([^"]+)"/)
|
|
265
|
+
const useName = useMatch ? useMatch[1].trim().split(/\s+/)[0] : null
|
|
266
|
+
|
|
267
|
+
// Deprecated or hidden commands are excluded from `rpk --print-tree`
|
|
268
|
+
const excluded = /\bHidden:\s*true\b/.test(body) ||
|
|
269
|
+
/\bDeprecated:\s*"/.test(body) ||
|
|
270
|
+
/\bDeprecateCmd\s*\(/.test(body)
|
|
271
|
+
|
|
272
|
+
constructors[matches[i].name] = { useName, excluded }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return constructors
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Extract constructor references from all AddCommand(...) calls in a file.
|
|
280
|
+
* Handles both same-package (`NewStartCommand(...)`) and cross-package
|
|
281
|
+
* (`tune.NewCommand(...)`) references. Only functions starting with
|
|
282
|
+
* New/new are considered, which skips wrappers like cobraext.DeprecateCmd.
|
|
283
|
+
* @param {string} content - Go file content
|
|
284
|
+
* @returns {Array<{qualifier: string|null, funcName: string}>}
|
|
285
|
+
*/
|
|
286
|
+
function parseAddCommandRefs(content) {
|
|
287
|
+
const refs = []
|
|
288
|
+
const addRe = /\bAddCommand\s*\(/g
|
|
289
|
+
let m
|
|
290
|
+
while ((m = addRe.exec(content)) !== null) {
|
|
291
|
+
// Find the span of this AddCommand(...) call by balancing parentheses
|
|
292
|
+
let depth = 1
|
|
293
|
+
let i = m.index + m[0].length
|
|
294
|
+
while (i < content.length && depth > 0) {
|
|
295
|
+
if (content[i] === '(') depth++
|
|
296
|
+
else if (content[i] === ')') depth--
|
|
297
|
+
i++
|
|
298
|
+
}
|
|
299
|
+
const span = content.slice(m.index + m[0].length, i - 1)
|
|
300
|
+
|
|
301
|
+
const callRe = /(?:(\w+)\.)?((?:New|new)\w*)\s*\(/g
|
|
302
|
+
let c
|
|
303
|
+
while ((c = callRe.exec(span)) !== null) {
|
|
304
|
+
refs.push({ qualifier: c[1] || null, funcName: c[2] })
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return refs
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Recursively collect non-test Go files under a directory.
|
|
312
|
+
* @param {string} dir - Absolute directory path
|
|
313
|
+
* @param {Array} out - Accumulator of {absPath, dir, name}
|
|
314
|
+
*/
|
|
315
|
+
function collectGoFiles(dir, out) {
|
|
316
|
+
let entries
|
|
317
|
+
try {
|
|
318
|
+
entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
319
|
+
} catch (err) {
|
|
320
|
+
return
|
|
321
|
+
}
|
|
322
|
+
for (const entry of entries) {
|
|
323
|
+
const fullPath = path.join(dir, entry.name)
|
|
324
|
+
if (entry.isDirectory()) {
|
|
325
|
+
if (entry.name === 'testdata' || entry.name === 'vendor') continue
|
|
326
|
+
collectGoFiles(fullPath, out)
|
|
327
|
+
} else if (entry.name.endsWith('.go') && !entry.name.endsWith('_test.go')) {
|
|
328
|
+
out.push({ absPath: fullPath, dir, name: entry.name })
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Build the command path prefix for a package directory.
|
|
335
|
+
* pkg/cli -> 'rpk'; pkg/cli/redpanda -> 'rpk redpanda';
|
|
336
|
+
* pkg/cli/cmd/redpanda (legacy) -> 'rpk redpanda'.
|
|
337
|
+
* @param {string} scanRoot - Absolute path of the scan root
|
|
338
|
+
* @param {string} dir - Absolute directory path
|
|
339
|
+
* @returns {string|null} Command path prefix, or null for non-command dirs
|
|
340
|
+
*/
|
|
341
|
+
function commandPrefixForDir(scanRoot, dir) {
|
|
342
|
+
const rel = path.relative(scanRoot, dir)
|
|
343
|
+
let segments = rel === '' ? [] : rel.split(path.sep)
|
|
344
|
+
// Legacy layouts nested commands under an extra cmd/ level
|
|
345
|
+
if (segments[0] === 'cmd') segments = segments.slice(1)
|
|
346
|
+
if (segments.some(s => NON_COMMAND_SEGMENTS.has(s))) return null
|
|
347
|
+
return ['rpk', ...segments].join(' ')
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Analyze one scan root and add detected Linux-only command paths.
|
|
352
|
+
* @param {string} scanRoot - Absolute path of the scan root
|
|
353
|
+
* @param {Set<string>} linuxOnlyCommands - Accumulator
|
|
354
|
+
*/
|
|
355
|
+
function scanRootForLinuxOnly(scanRoot, linuxOnlyCommands) {
|
|
356
|
+
const files = []
|
|
357
|
+
collectGoFiles(scanRoot, files)
|
|
358
|
+
if (files.length === 0) return
|
|
359
|
+
|
|
360
|
+
// Group files (with parsed metadata) by directory
|
|
361
|
+
const dirs = new Map()
|
|
362
|
+
for (const file of files) {
|
|
363
|
+
let content
|
|
364
|
+
try {
|
|
365
|
+
content = fs.readFileSync(file.absPath, 'utf8')
|
|
366
|
+
} catch (err) {
|
|
367
|
+
continue
|
|
368
|
+
}
|
|
369
|
+
const info = {
|
|
370
|
+
name: file.name,
|
|
371
|
+
content,
|
|
372
|
+
buildsOnLinux: fileBuildsOn(file.name, content, 'linux'),
|
|
373
|
+
buildsOnDarwin: fileBuildsOn(file.name, content, 'darwin')
|
|
374
|
+
}
|
|
375
|
+
if (!dirs.has(file.dir)) dirs.set(file.dir, [])
|
|
376
|
+
dirs.get(file.dir).push(info)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Per-directory constructor index: dir -> {funcName: {useName, excluded}}
|
|
380
|
+
const constructorIndex = new Map()
|
|
381
|
+
for (const [dir, dirFiles] of dirs) {
|
|
382
|
+
const index = {}
|
|
383
|
+
for (const f of dirFiles) {
|
|
384
|
+
Object.assign(index, parseConstructors(f.content))
|
|
385
|
+
}
|
|
386
|
+
constructorIndex.set(dir, index)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Resolve an AddCommand constructor reference to a visible command name.
|
|
391
|
+
* Returns null for deprecated/hidden/unresolvable commands.
|
|
392
|
+
*/
|
|
393
|
+
const resolveRef = (dir, ref) => {
|
|
394
|
+
let target = null
|
|
395
|
+
let fallback = commandNameFromConstructor(ref.funcName)
|
|
396
|
+
|
|
397
|
+
if (ref.qualifier) {
|
|
398
|
+
const subDir = path.join(dir, ref.qualifier)
|
|
399
|
+
target = constructorIndex.get(subDir) || null
|
|
400
|
+
if (!fallback) fallback = ref.qualifier
|
|
401
|
+
} else {
|
|
402
|
+
target = constructorIndex.get(dir) || null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (target && target[ref.funcName]) {
|
|
406
|
+
const ctor = target[ref.funcName]
|
|
407
|
+
if (ctor.excluded) return null
|
|
408
|
+
return ctor.useName || fallback
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Unresolvable reference: only trust it if the function name follows
|
|
412
|
+
// the cobra constructor convention (New[Xxx]Command). This skips
|
|
413
|
+
// helper arguments captured inside AddCommand spans, such as the
|
|
414
|
+
// rp.NewLauncher() argument in redpanda.NewCommand(fs, p, rp.NewLauncher()).
|
|
415
|
+
if (!/^(?:New|new)(?:\w*(?:Command|Cmd))?$/.test(ref.funcName)) return null
|
|
416
|
+
return fallback
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
for (const [dir, dirFiles] of dirs) {
|
|
420
|
+
const prefix = commandPrefixForDir(scanRoot, dir)
|
|
421
|
+
if (!prefix) continue
|
|
422
|
+
|
|
423
|
+
const hasDifferentialFile = dirFiles.some(f => f.buildsOnLinux !== f.buildsOnDarwin)
|
|
424
|
+
|
|
425
|
+
// Pattern 1: whole package is Linux-gated (and defines a command)
|
|
426
|
+
if (prefix !== 'rpk') {
|
|
427
|
+
const anyLinux = dirFiles.some(f => f.buildsOnLinux)
|
|
428
|
+
const anyDarwin = dirFiles.some(f => f.buildsOnDarwin)
|
|
429
|
+
const definesCommand = Object.keys(constructorIndex.get(dir)).length > 0
|
|
430
|
+
if (anyLinux && !anyDarwin && definesCommand) {
|
|
431
|
+
linuxOnlyCommands.add(prefix)
|
|
432
|
+
continue
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// Pattern 2: dual registration - diff subcommands registered from
|
|
437
|
+
// linux-buildable files vs darwin-buildable files
|
|
438
|
+
if (!hasDifferentialFile) continue
|
|
439
|
+
|
|
440
|
+
const refsFor = (goos) => {
|
|
441
|
+
const names = new Set()
|
|
442
|
+
for (const f of dirFiles) {
|
|
443
|
+
const buildable = goos === 'linux' ? f.buildsOnLinux : f.buildsOnDarwin
|
|
444
|
+
if (!buildable) continue
|
|
445
|
+
for (const ref of parseAddCommandRefs(f.content)) {
|
|
446
|
+
const name = resolveRef(dir, ref)
|
|
447
|
+
if (name) names.add(name)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return names
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const linuxRefs = refsFor('linux')
|
|
454
|
+
const darwinRefs = refsFor('darwin')
|
|
455
|
+
|
|
456
|
+
for (const name of linuxRefs) {
|
|
457
|
+
if (!darwinRefs.has(name)) {
|
|
458
|
+
linuxOnlyCommands.add(`${prefix} ${name}`)
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Detect Linux-only rpk commands by statically analyzing Go source.
|
|
466
|
+
* Works on any platform, including Linux CI runners.
|
|
467
|
+
* @param {string} sourcePath - Path to the rpk Go source root (src/go/rpk)
|
|
468
|
+
* @returns {Set<string>} Linux-only command paths (e.g. 'rpk iotune').
|
|
469
|
+
* Descendants of a returned path are implicitly Linux-only too.
|
|
470
|
+
*/
|
|
471
|
+
function detectLinuxOnlyFromSource(sourcePath) {
|
|
472
|
+
const linuxOnlyCommands = new Set()
|
|
473
|
+
|
|
474
|
+
const scannedRoots = new Set()
|
|
475
|
+
for (const root of SCAN_ROOTS) {
|
|
476
|
+
const absRoot = path.join(sourcePath, root)
|
|
477
|
+
if (!fs.existsSync(absRoot)) continue
|
|
478
|
+
// pkg/cli/cmd is nested inside pkg/cli; avoid scanning it twice
|
|
479
|
+
if ([...scannedRoots].some(r => absRoot.startsWith(r + path.sep))) continue
|
|
480
|
+
scannedRoots.add(absRoot)
|
|
481
|
+
scanRootForLinuxOnly(absRoot, linuxOnlyCommands)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Drop any detected path that is a descendant of another detected path;
|
|
485
|
+
// platform markers already propagate to descendants.
|
|
486
|
+
for (const cmd of [...linuxOnlyCommands]) {
|
|
487
|
+
if ([...linuxOnlyCommands].some(other => other !== cmd && cmd.startsWith(other + ' '))) {
|
|
488
|
+
linuxOnlyCommands.delete(cmd)
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return linuxOnlyCommands
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* List Go files under the scanned roots that carry a Linux-only build
|
|
497
|
+
* constraint (explicit tag or filename suffix). Used as a tripwire: if
|
|
498
|
+
* detection returns an empty set while this list is non-empty, the scan
|
|
499
|
+
* is almost certainly broken (wrong directory layout, changed tag style).
|
|
500
|
+
* @param {string} sourcePath - Path to the rpk Go source root
|
|
501
|
+
* @returns {string[]} Relative paths of Linux-constrained files
|
|
502
|
+
*/
|
|
503
|
+
function findLinuxConstrainedFiles(sourcePath) {
|
|
504
|
+
const results = []
|
|
505
|
+
for (const root of SCAN_ROOTS) {
|
|
506
|
+
const absRoot = path.join(sourcePath, root)
|
|
507
|
+
if (!fs.existsSync(absRoot)) continue
|
|
508
|
+
const files = []
|
|
509
|
+
collectGoFiles(absRoot, files)
|
|
510
|
+
for (const file of files) {
|
|
511
|
+
let content
|
|
512
|
+
try {
|
|
513
|
+
content = fs.readFileSync(file.absPath, 'utf8')
|
|
514
|
+
} catch (err) {
|
|
515
|
+
continue
|
|
516
|
+
}
|
|
517
|
+
if (fileBuildsOn(file.name, content, 'linux') && !fileBuildsOn(file.name, content, 'darwin')) {
|
|
518
|
+
results.push(path.relative(sourcePath, file.absPath))
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
return [...new Set(results)]
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Emit a loud warning when platform detection came back empty even though
|
|
527
|
+
* the source demonstrably contains Linux-gated files. Never let a scan
|
|
528
|
+
* failure silently mark every command as cross-platform.
|
|
529
|
+
* @param {string} sourcePath - Path to the rpk Go source root
|
|
530
|
+
* @param {Set<string>} linuxOnlyCommands - Combined detection result
|
|
531
|
+
* @returns {boolean} True when the tripwire fired
|
|
532
|
+
*/
|
|
533
|
+
function warnIfDetectionLooksBroken(sourcePath, linuxOnlyCommands) {
|
|
534
|
+
if (linuxOnlyCommands.size > 0) return false
|
|
535
|
+
|
|
536
|
+
const constrained = findLinuxConstrainedFiles(sourcePath)
|
|
537
|
+
if (constrained.length === 0) return false
|
|
538
|
+
|
|
539
|
+
console.warn('\n' + '!'.repeat(70))
|
|
540
|
+
console.warn('⚠ PLATFORM DETECTION TRIPWIRE: no Linux-only commands were detected,')
|
|
541
|
+
console.warn(`⚠ but the rpk source contains ${constrained.length} Linux-constrained file(s), e.g.:`)
|
|
542
|
+
for (const file of constrained.slice(0, 5)) {
|
|
543
|
+
console.warn(`⚠ - ${file}`)
|
|
544
|
+
}
|
|
545
|
+
console.warn('⚠ This usually means the static source scan failed (unexpected')
|
|
546
|
+
console.warn('⚠ directory layout or build-tag style) and every command is about')
|
|
547
|
+
console.warn('⚠ to be marked as available on both Linux and macOS, which is wrong.')
|
|
548
|
+
console.warn('⚠ Fix tools/rpk-docs/detect-platform-commands.js before publishing')
|
|
549
|
+
console.warn('⚠ these docs. See redpanda-data/docs#1831 for the impact of this.')
|
|
550
|
+
console.warn('!'.repeat(70) + '\n')
|
|
551
|
+
return true
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
module.exports = {
|
|
555
|
+
detectLinuxOnlyFromSource,
|
|
556
|
+
findLinuxConstrainedFiles,
|
|
557
|
+
warnIfDetectionLooksBroken,
|
|
558
|
+
// Exported for unit tests
|
|
559
|
+
fileBuildsOn,
|
|
560
|
+
goosFromFileName,
|
|
561
|
+
evaluateBuildExpr,
|
|
562
|
+
evaluatePlusBuildLines,
|
|
563
|
+
parseConstructors,
|
|
564
|
+
parseAddCommandRefs,
|
|
565
|
+
commandNameFromConstructor
|
|
566
|
+
}
|