socket 1.1.102 → 1.1.104
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/CHANGELOG.md +14 -0
- package/dist/cli.js +1311 -174
- package/dist/cli.js.map +1 -1
- package/dist/constants.js +4 -4
- package/dist/constants.js.map +1 -1
- package/dist/tsconfig.dts.tsbuildinfo +1 -1
- package/dist/types/commands/manifest/bazel/bazel-pypi-discovery.d.mts +31 -0
- package/dist/types/commands/manifest/bazel/bazel-pypi-discovery.d.mts.map +1 -0
- package/dist/types/commands/manifest/bazel/bazel-pypi-parser.d.mts +46 -0
- package/dist/types/commands/manifest/bazel/bazel-pypi-parser.d.mts.map +1 -0
- package/dist/types/commands/manifest/bazel/bazel-query-runner.d.mts +16 -2
- package/dist/types/commands/manifest/bazel/bazel-query-runner.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/bazel-repo-discovery.d.mts +3 -3
- package/dist/types/commands/manifest/bazel/bazel-repo-discovery.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/cmd-manifest-bazel.d.mts +19 -0
- package/dist/types/commands/manifest/bazel/cmd-manifest-bazel.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/extract_bazel_to_maven.d.mts +1 -0
- package/dist/types/commands/manifest/bazel/extract_bazel_to_maven.d.mts.map +1 -1
- package/dist/types/commands/manifest/bazel/extract_bazel_to_pypi.d.mts +20 -0
- package/dist/types/commands/manifest/bazel/extract_bazel_to_pypi.d.mts.map +1 -0
- package/dist/types/commands/manifest/generate_auto_manifest.d.mts.map +1 -1
- package/dist/types/utils/dlx.d.mts.map +1 -1
- package/dist/utils.js +27 -3
- package/dist/utils.js.map +1 -1
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -2245,6 +2245,8 @@ async function provisionPythonShim() {
|
|
|
2245
2245
|
// Default per-invocation timeout for bazel queries. Bazel cold-cache starts
|
|
2246
2246
|
// can take several minutes; 10 minutes is generous while still bounding CI hangs.
|
|
2247
2247
|
const BAZEL_QUERY_TIMEOUT_MS = 600_000;
|
|
2248
|
+
const STDERR_TAIL_BYTES = 4_096;
|
|
2249
|
+
const STDOUT_EXCERPT_BYTES = 1_024;
|
|
2248
2250
|
|
|
2249
2251
|
// Splits the user-supplied --bazel-flags string on whitespace.
|
|
2250
2252
|
// Empty / undefined returns []. No shell parsing — quoted args with embedded
|
|
@@ -2265,11 +2267,22 @@ function buildBazelModShowVisibleReposArgv(opts) {
|
|
|
2265
2267
|
startup.push(`--output_base=${opts.bazelOutputBase}`);
|
|
2266
2268
|
}
|
|
2267
2269
|
const userFlags = splitBazelFlags(opts.bazelFlags);
|
|
2268
|
-
return [...startup, 'mod', '
|
|
2270
|
+
return [...startup, 'mod', 'dump_repo_mapping', '', '--output=json', ...userFlags];
|
|
2269
2271
|
}
|
|
2270
|
-
function
|
|
2272
|
+
function buildBazelModShowPipExtensionArgv(opts) {
|
|
2273
|
+
const startup = [];
|
|
2274
|
+
if (opts.bazelRc) {
|
|
2275
|
+
startup.push(`--bazelrc=${opts.bazelRc}`);
|
|
2276
|
+
}
|
|
2277
|
+
if (opts.bazelOutputBase) {
|
|
2278
|
+
startup.push(`--output_base=${opts.bazelOutputBase}`);
|
|
2279
|
+
}
|
|
2280
|
+
const userFlags = splitBazelFlags(opts.bazelFlags);
|
|
2281
|
+
return [...startup, 'mod', 'show_extension', '@rules_python//python/extensions:pip.bzl%pip', '--extension_usages=<root>', ...userFlags];
|
|
2282
|
+
}
|
|
2283
|
+
function buildBazelArgv(queryStr, opts, output = 'build') {
|
|
2271
2284
|
// Startup flags MUST precede the `query` subcommand.
|
|
2272
|
-
// Bazel argv shape: <startup> query <queryFlags> <invocationFlags> <queryStr> --output
|
|
2285
|
+
// Bazel argv shape: <startup> query <queryFlags> <invocationFlags> <queryStr> --output=<output> <userFlags>
|
|
2273
2286
|
const startup = [];
|
|
2274
2287
|
if (opts.bazelRc) {
|
|
2275
2288
|
startup.push(`--bazelrc=${opts.bazelRc}`);
|
|
@@ -2280,7 +2293,7 @@ function buildBazelArgv(queryStr, opts) {
|
|
|
2280
2293
|
// Keep query output stable and avoid updating Bazel lockfiles while extracting.
|
|
2281
2294
|
const queryFlags = ['--lockfile_mode=off', '--noshow_progress'];
|
|
2282
2295
|
const userFlags = splitBazelFlags(opts.bazelFlags);
|
|
2283
|
-
return [...startup, 'query', ...queryFlags, ...opts.invocationFlags, queryStr,
|
|
2296
|
+
return [...startup, 'query', ...queryFlags, ...opts.invocationFlags, queryStr, `--output=${output}`, ...userFlags];
|
|
2284
2297
|
}
|
|
2285
2298
|
function stringField(value) {
|
|
2286
2299
|
return typeof value === 'string' ? value : '';
|
|
@@ -2288,6 +2301,46 @@ function stringField(value) {
|
|
|
2288
2301
|
function numericExitCode(value) {
|
|
2289
2302
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
2290
2303
|
}
|
|
2304
|
+
function byteLength(value) {
|
|
2305
|
+
return Buffer.byteLength(value, 'utf8');
|
|
2306
|
+
}
|
|
2307
|
+
function excerpt(value, maxBytes) {
|
|
2308
|
+
if (byteLength(value) <= maxBytes) {
|
|
2309
|
+
return value;
|
|
2310
|
+
}
|
|
2311
|
+
return value.slice(0, maxBytes) + '\n[truncated]';
|
|
2312
|
+
}
|
|
2313
|
+
function logBazelTrace({
|
|
2314
|
+
argv,
|
|
2315
|
+
durationMs,
|
|
2316
|
+
opts,
|
|
2317
|
+
result,
|
|
2318
|
+
step
|
|
2319
|
+
}) {
|
|
2320
|
+
if (!opts.verbose) {
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
const stderrBytes = byteLength(result.stderr);
|
|
2324
|
+
const stdoutBytes = byteLength(result.stdout);
|
|
2325
|
+
const category = result.code === 0 ? 'ok' : 'bazel-query-failed';
|
|
2326
|
+
logger.logger.log('[VERBOSE] bazel subprocess trace:', `category=${category}`, {
|
|
2327
|
+
argv,
|
|
2328
|
+
category,
|
|
2329
|
+
code: result.code,
|
|
2330
|
+
cwd: opts.cwd,
|
|
2331
|
+
durationMs,
|
|
2332
|
+
stderrBytes,
|
|
2333
|
+
stdoutBytes,
|
|
2334
|
+
step,
|
|
2335
|
+
timedOut: false,
|
|
2336
|
+
timeoutMs: BAZEL_QUERY_TIMEOUT_MS
|
|
2337
|
+
});
|
|
2338
|
+
if (result.code !== 0 && result.stderr) {
|
|
2339
|
+
logger.logger.log('[VERBOSE] bazel stderr tail:', excerpt(result.stderr.slice(-STDERR_TAIL_BYTES), STDERR_TAIL_BYTES));
|
|
2340
|
+
} else if (result.stdout && stdoutBytes <= STDOUT_EXCERPT_BYTES) {
|
|
2341
|
+
logger.logger.log('[VERBOSE] bazel stdout excerpt:', result.stdout);
|
|
2342
|
+
}
|
|
2343
|
+
}
|
|
2291
2344
|
function normalizeSpawnError(error) {
|
|
2292
2345
|
const e = error;
|
|
2293
2346
|
return {
|
|
@@ -2303,11 +2356,12 @@ function normalizeSpawnError(error) {
|
|
|
2303
2356
|
* and fails on non-zero exit. Rejected spawn calls are normalized into a
|
|
2304
2357
|
* BazelQueryResult so retry/skip handling can inspect stderr.
|
|
2305
2358
|
*/
|
|
2306
|
-
async function runBazelQuery(queryStr, opts) {
|
|
2307
|
-
const argv = buildBazelArgv(queryStr, opts);
|
|
2359
|
+
async function runBazelQuery(queryStr, opts, output) {
|
|
2360
|
+
const argv = buildBazelArgv(queryStr, opts, output);
|
|
2308
2361
|
if (opts.verbose) {
|
|
2309
2362
|
logger.logger.log('[VERBOSE] Executing:', opts.bin, ', args:', argv);
|
|
2310
2363
|
}
|
|
2364
|
+
const startedAt = Date.now();
|
|
2311
2365
|
const {
|
|
2312
2366
|
spinner
|
|
2313
2367
|
} = constants.default;
|
|
@@ -2342,19 +2396,30 @@ async function runBazelQuery(queryStr, opts) {
|
|
|
2342
2396
|
} else {
|
|
2343
2397
|
spinner.failAndStop(`bazel query failed (${truncated}).`);
|
|
2344
2398
|
}
|
|
2399
|
+
if (result) {
|
|
2400
|
+
logBazelTrace({
|
|
2401
|
+
argv,
|
|
2402
|
+
durationMs: Date.now() - startedAt,
|
|
2403
|
+
opts,
|
|
2404
|
+
result,
|
|
2405
|
+
step: `bazel query ${truncated}`
|
|
2406
|
+
});
|
|
2407
|
+
}
|
|
2345
2408
|
}
|
|
2346
2409
|
}
|
|
2347
2410
|
|
|
2348
2411
|
/**
|
|
2349
2412
|
* Bzlmod-native visible repository enumeration. This is only a candidate
|
|
2350
2413
|
* source; callers must still validate each returned apparent repo name with a
|
|
2351
|
-
* semantic query for generated
|
|
2414
|
+
* semantic query for generated ecosystem rules.
|
|
2352
2415
|
*/
|
|
2353
2416
|
async function runBazelModShowVisibleRepos(opts) {
|
|
2354
2417
|
const argv = buildBazelModShowVisibleReposArgv(opts);
|
|
2355
2418
|
if (opts.verbose) {
|
|
2356
2419
|
logger.logger.log('[VERBOSE] Executing:', opts.bin, ', args:', argv);
|
|
2357
2420
|
}
|
|
2421
|
+
const startedAt = Date.now();
|
|
2422
|
+
let result;
|
|
2358
2423
|
try {
|
|
2359
2424
|
const output = await spawn.spawn(opts.bin, argv, {
|
|
2360
2425
|
cwd: opts.cwd,
|
|
@@ -2368,14 +2433,65 @@ async function runBazelModShowVisibleRepos(opts) {
|
|
|
2368
2433
|
stderr,
|
|
2369
2434
|
stdout
|
|
2370
2435
|
} = output;
|
|
2371
|
-
|
|
2436
|
+
result = {
|
|
2437
|
+
code,
|
|
2438
|
+
stdout,
|
|
2439
|
+
stderr
|
|
2440
|
+
};
|
|
2441
|
+
} catch (e) {
|
|
2442
|
+
result = normalizeSpawnError(e);
|
|
2443
|
+
}
|
|
2444
|
+
logBazelTrace({
|
|
2445
|
+
argv,
|
|
2446
|
+
durationMs: Date.now() - startedAt,
|
|
2447
|
+
opts,
|
|
2448
|
+
result,
|
|
2449
|
+
step: 'bazel mod dump_repo_mapping'
|
|
2450
|
+
});
|
|
2451
|
+
return result;
|
|
2452
|
+
}
|
|
2453
|
+
|
|
2454
|
+
/**
|
|
2455
|
+
* Bzlmod-native rules_python pip extension usage inspection. This is the
|
|
2456
|
+
* authoritative source for root-module pip.parse metadata when Bazel supports
|
|
2457
|
+
* the command; callers keep bounded static parsing as fallback.
|
|
2458
|
+
*/
|
|
2459
|
+
async function runBazelModShowPipExtension(opts) {
|
|
2460
|
+
const argv = buildBazelModShowPipExtensionArgv(opts);
|
|
2461
|
+
if (opts.verbose) {
|
|
2462
|
+
logger.logger.log('[VERBOSE] Executing:', opts.bin, ', args:', argv);
|
|
2463
|
+
}
|
|
2464
|
+
const startedAt = Date.now();
|
|
2465
|
+
let result;
|
|
2466
|
+
try {
|
|
2467
|
+
const output = await spawn.spawn(opts.bin, argv, {
|
|
2468
|
+
cwd: opts.cwd,
|
|
2469
|
+
timeout: BAZEL_QUERY_TIMEOUT_MS,
|
|
2470
|
+
...(opts.env ? {
|
|
2471
|
+
env: opts.env
|
|
2472
|
+
} : {})
|
|
2473
|
+
});
|
|
2474
|
+
const {
|
|
2475
|
+
code,
|
|
2476
|
+
stderr,
|
|
2477
|
+
stdout
|
|
2478
|
+
} = output;
|
|
2479
|
+
result = {
|
|
2372
2480
|
code,
|
|
2373
2481
|
stdout,
|
|
2374
2482
|
stderr
|
|
2375
2483
|
};
|
|
2376
2484
|
} catch (e) {
|
|
2377
|
-
|
|
2485
|
+
result = normalizeSpawnError(e);
|
|
2378
2486
|
}
|
|
2487
|
+
logBazelTrace({
|
|
2488
|
+
argv,
|
|
2489
|
+
durationMs: Date.now() - startedAt,
|
|
2490
|
+
opts,
|
|
2491
|
+
result,
|
|
2492
|
+
step: 'bazel mod show_extension rules_python pip'
|
|
2493
|
+
});
|
|
2494
|
+
return result;
|
|
2379
2495
|
}
|
|
2380
2496
|
|
|
2381
2497
|
/**
|
|
@@ -2394,13 +2510,31 @@ function buildProbeFor(opts) {
|
|
|
2394
2510
|
};
|
|
2395
2511
|
}
|
|
2396
2512
|
|
|
2513
|
+
/**
|
|
2514
|
+
* Build a `RepoProbe` for validating pip hub candidates.
|
|
2515
|
+
* Queries the hub for package targets (e.g. `@<hub>//...`) and returns
|
|
2516
|
+
* stdout so the caller can check for `:pkg` labels or alias rules.
|
|
2517
|
+
* Does NOT require `pypi_name=` tags in the hub output, because those
|
|
2518
|
+
* tags live on spoke repos, not the hub alias layer.
|
|
2519
|
+
*/
|
|
2520
|
+
function buildPypiProbeFor(opts) {
|
|
2521
|
+
return async hubName => {
|
|
2522
|
+
const queryStr = `@${hubName}//...`;
|
|
2523
|
+
const result = await runBazelQuery(queryStr, opts);
|
|
2524
|
+
return {
|
|
2525
|
+
stdout: result.stdout,
|
|
2526
|
+
code: result.code
|
|
2527
|
+
};
|
|
2528
|
+
};
|
|
2529
|
+
}
|
|
2530
|
+
|
|
2397
2531
|
// Maximum size (bytes) we will read for any single Bazel workspace file.
|
|
2398
2532
|
// Prevents DoS via maliciously large MODULE.bazel / WORKSPACE / .bzl files.
|
|
2399
|
-
const MAX_WORKSPACE_FILE_BYTES = 5 * 1024 * 1024;
|
|
2533
|
+
const MAX_WORKSPACE_FILE_BYTES$1 = 5 * 1024 * 1024;
|
|
2400
2534
|
|
|
2401
2535
|
// Maximum candidate count we will return (deduped) before truncating.
|
|
2402
2536
|
// Real repos have <20; this is a hard ceiling against pathological inputs.
|
|
2403
|
-
const MAX_CANDIDATES = 256;
|
|
2537
|
+
const MAX_CANDIDATES$1 = 256;
|
|
2404
2538
|
|
|
2405
2539
|
// Regex strategy: anchored, bounded character classes, no nested quantifiers.
|
|
2406
2540
|
// Match `use_repo(maven, "X", "Y", ...)` with a bounded arg-list window to
|
|
@@ -2421,13 +2555,13 @@ const MAVEN_COORDINATES_MARKER_RE = /\bmaven_coordinates\s*=/;
|
|
|
2421
2555
|
|
|
2422
2556
|
// Reads file contents, refusing files that exceed MAX_WORKSPACE_FILE_BYTES.
|
|
2423
2557
|
// Returns null when the file is missing, oversized, or unreadable.
|
|
2424
|
-
function safeReadFile(file) {
|
|
2558
|
+
function safeReadFile$1(file) {
|
|
2425
2559
|
if (!fs$1.existsSync(file)) {
|
|
2426
2560
|
return null;
|
|
2427
2561
|
}
|
|
2428
2562
|
try {
|
|
2429
2563
|
const stat = fs$1.statSync(file);
|
|
2430
|
-
if (stat.size > MAX_WORKSPACE_FILE_BYTES) {
|
|
2564
|
+
if (stat.size > MAX_WORKSPACE_FILE_BYTES$1) {
|
|
2431
2565
|
return null;
|
|
2432
2566
|
}
|
|
2433
2567
|
return fs$1.readFileSync(file, 'utf8');
|
|
@@ -2439,7 +2573,7 @@ function safeReadFile(file) {
|
|
|
2439
2573
|
// Walks workspace root for legacy Starlark sources we can scan: WORKSPACE
|
|
2440
2574
|
// (and WORKSPACE.bazel) plus top-level .bzl files. Non-recursive by design;
|
|
2441
2575
|
// Phase 1 explicitly avoids static Starlark parsing at depth.
|
|
2442
|
-
function listLegacyStarlarkFiles(cwd) {
|
|
2576
|
+
function listLegacyStarlarkFiles$1(cwd) {
|
|
2443
2577
|
const files = [];
|
|
2444
2578
|
const candidates = ['WORKSPACE', 'WORKSPACE.bazel'];
|
|
2445
2579
|
for (const c of candidates) {
|
|
@@ -2469,7 +2603,7 @@ function uniqueSorted(items) {
|
|
|
2469
2603
|
if (!seen.has(item)) {
|
|
2470
2604
|
seen.add(item);
|
|
2471
2605
|
out.push(item);
|
|
2472
|
-
if (out.length >= MAX_CANDIDATES) {
|
|
2606
|
+
if (out.length >= MAX_CANDIDATES$1) {
|
|
2473
2607
|
break;
|
|
2474
2608
|
}
|
|
2475
2609
|
}
|
|
@@ -2493,14 +2627,29 @@ function apparentNameFromJsonValue(value) {
|
|
|
2493
2627
|
}
|
|
2494
2628
|
return undefined;
|
|
2495
2629
|
}
|
|
2630
|
+
function apparentNamesFromRepoMapping(value) {
|
|
2631
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
2632
|
+
return [];
|
|
2633
|
+
}
|
|
2634
|
+
const candidates = [];
|
|
2635
|
+
for (const [name, canonicalName] of Object.entries(value)) {
|
|
2636
|
+
if (name.startsWith('@') || typeof canonicalName !== 'string') {
|
|
2637
|
+
continue;
|
|
2638
|
+
}
|
|
2639
|
+
if (BAZEL_REPO_NAME_RE.test(name)) {
|
|
2640
|
+
candidates.push(name);
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
return candidates;
|
|
2644
|
+
}
|
|
2496
2645
|
function normalizeRepoName(name) {
|
|
2497
2646
|
const repo = name.startsWith('@') ? name.slice(1) : name;
|
|
2498
2647
|
return BAZEL_REPO_NAME_RE.test(repo) ? repo : undefined;
|
|
2499
2648
|
}
|
|
2500
2649
|
|
|
2501
|
-
// Parse `bazel mod
|
|
2502
|
-
//
|
|
2503
|
-
//
|
|
2650
|
+
// Parse `bazel mod dump_repo_mapping "" --output=json` output. Also accept the
|
|
2651
|
+
// older streamed jsonproto shape in case older Bazel versions or fixtures still
|
|
2652
|
+
// return repository records with apparentName fields.
|
|
2504
2653
|
function parseVisibleRepoCandidates(output) {
|
|
2505
2654
|
const candidates = [];
|
|
2506
2655
|
for (const line of output.split(/\r?\n/)) {
|
|
@@ -2510,6 +2659,7 @@ function parseVisibleRepoCandidates(output) {
|
|
|
2510
2659
|
}
|
|
2511
2660
|
try {
|
|
2512
2661
|
const parsed = JSON.parse(trimmed);
|
|
2662
|
+
candidates.push(...apparentNamesFromRepoMapping(parsed));
|
|
2513
2663
|
const apparentName = apparentNameFromJsonValue(parsed);
|
|
2514
2664
|
if (apparentName) {
|
|
2515
2665
|
const repo = normalizeRepoName(apparentName);
|
|
@@ -2531,7 +2681,7 @@ function parseMavenRepoCandidates(cwd, verbose) {
|
|
|
2531
2681
|
|
|
2532
2682
|
// Bzlmod path: parse MODULE.bazel for use_repo(maven, ...).
|
|
2533
2683
|
const moduleBazel = path.join(cwd, 'MODULE.bazel');
|
|
2534
|
-
const moduleContent = safeReadFile(moduleBazel);
|
|
2684
|
+
const moduleContent = safeReadFile$1(moduleBazel);
|
|
2535
2685
|
if (moduleContent) {
|
|
2536
2686
|
const bzlmodHits = [];
|
|
2537
2687
|
for (const m of moduleContent.matchAll(USE_REPO_RE)) {
|
|
@@ -2549,12 +2699,12 @@ function parseMavenRepoCandidates(cwd, verbose) {
|
|
|
2549
2699
|
}
|
|
2550
2700
|
|
|
2551
2701
|
// Legacy path: scan WORKSPACE + top-level .bzl files for maven_install(name=...).
|
|
2552
|
-
const legacyFiles = listLegacyStarlarkFiles(cwd);
|
|
2702
|
+
const legacyFiles = listLegacyStarlarkFiles$1(cwd);
|
|
2553
2703
|
if (verbose) {
|
|
2554
2704
|
logger.logger.log('[VERBOSE] discovery: legacy files considered:', legacyFiles.length ? legacyFiles : '(none)');
|
|
2555
2705
|
}
|
|
2556
2706
|
for (const file of legacyFiles) {
|
|
2557
|
-
const content = safeReadFile(file);
|
|
2707
|
+
const content = safeReadFile$1(file);
|
|
2558
2708
|
if (!content) {
|
|
2559
2709
|
continue;
|
|
2560
2710
|
}
|
|
@@ -3025,8 +3175,18 @@ async function extractBazelToMaven(opts) {
|
|
|
3025
3175
|
});
|
|
3026
3176
|
}
|
|
3027
3177
|
if (!allArtifacts.length) {
|
|
3028
|
-
|
|
3029
|
-
|
|
3178
|
+
if (!repos.size) {
|
|
3179
|
+
if (verbose) {
|
|
3180
|
+
logger.logger.info('No Maven artifacts extracted. failureCategory=no-supported-ecosystem');
|
|
3181
|
+
}
|
|
3182
|
+
return {
|
|
3183
|
+
artifactCount: 0,
|
|
3184
|
+
manifestPath,
|
|
3185
|
+
noEcosystemFound: true,
|
|
3186
|
+
ok: false
|
|
3187
|
+
};
|
|
3188
|
+
}
|
|
3189
|
+
logger.logger.fail(`Discovered Maven repo(s) ${repoNames.join(', ')} but extracted zero artifacts. failureCategory=ecosystem-detected-but-empty`);
|
|
3030
3190
|
return {
|
|
3031
3191
|
artifactCount: 0,
|
|
3032
3192
|
manifestPath,
|
|
@@ -3040,7 +3200,6 @@ async function extractBazelToMaven(opts) {
|
|
|
3040
3200
|
ok: true
|
|
3041
3201
|
};
|
|
3042
3202
|
} catch (e) {
|
|
3043
|
-
process.exitCode = 1;
|
|
3044
3203
|
// Always surface the error message; users should not have to
|
|
3045
3204
|
// re-run a multi-minute bazel build with --verbose just to see whether
|
|
3046
3205
|
// the failure was a missing dependency, permission error, or network blip.
|
|
@@ -3681,24 +3840,23 @@ async function generateAutoManifest({
|
|
|
3681
3840
|
if (!sockJson?.defaults?.manifest?.bazel?.disabled && detected.bazel) {
|
|
3682
3841
|
const bazelConfig = sockJson?.defaults?.manifest?.bazel;
|
|
3683
3842
|
logger.logger.log('Detected a Bazel workspace, extracting Maven dependencies via bazel query...');
|
|
3684
|
-
const
|
|
3843
|
+
const mavenResult = await extractBazelToMaven({
|
|
3685
3844
|
bazelFlags: bazelConfig?.bazelFlags,
|
|
3686
3845
|
bazelOutputBase: bazelConfig?.bazelOutputBase,
|
|
3687
3846
|
bazelRc: bazelConfig?.bazelRc,
|
|
3688
3847
|
bin: bazelConfig?.bazel ?? bazelConfig?.bin,
|
|
3689
3848
|
cwd,
|
|
3690
|
-
// Auto-manifest writes into a sibling directory instead of the repo root
|
|
3691
|
-
// so scan discovery can pick it up without colliding with a checked-in
|
|
3692
|
-
// rules_jvm_external lockfile or repo-root gitignore patterns.
|
|
3693
3849
|
out: bazelConfig?.out ?? cwd,
|
|
3694
3850
|
outLayout: 'flat',
|
|
3695
3851
|
verbose: Boolean(bazelConfig?.verbose) || verbose
|
|
3696
3852
|
});
|
|
3697
|
-
if (!
|
|
3698
|
-
throw new Error('Bazel auto-manifest generation failed');
|
|
3853
|
+
if (!mavenResult.ok && !mavenResult.noEcosystemFound) {
|
|
3854
|
+
throw new Error('Bazel auto-manifest generation failed for ecosystem(s): maven');
|
|
3699
3855
|
}
|
|
3700
|
-
if (
|
|
3701
|
-
generatedFiles.push(
|
|
3856
|
+
if (mavenResult.ok && mavenResult.manifestPath) {
|
|
3857
|
+
generatedFiles.push(mavenResult.manifestPath);
|
|
3858
|
+
} else if (mavenResult.noEcosystemFound) {
|
|
3859
|
+
logger.logger.info('No supported Bazel Maven ecosystem detected.');
|
|
3702
3860
|
}
|
|
3703
3861
|
}
|
|
3704
3862
|
return {
|
|
@@ -7302,149 +7460,1087 @@ async function run$G(argv, importMeta, context) {
|
|
|
7302
7460
|
await spawnPromise;
|
|
7303
7461
|
}
|
|
7304
7462
|
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
|
|
7308
|
-
hidden: false,
|
|
7309
|
-
flags: {
|
|
7310
|
-
...flags.commonFlags,
|
|
7311
|
-
bazel: {
|
|
7312
|
-
type: 'string',
|
|
7313
|
-
description: 'Path to bazel/bazelisk binary; default: $(which bazelisk) || $(which bazel)'
|
|
7314
|
-
},
|
|
7315
|
-
bazelFlags: {
|
|
7316
|
-
type: 'string',
|
|
7317
|
-
description: 'Flags forwarded to every bazel invocation (single quoted string)'
|
|
7318
|
-
},
|
|
7319
|
-
bazelOutputBase: {
|
|
7320
|
-
type: 'string',
|
|
7321
|
-
description: 'Bazel --output_base for read-only-cache CI environments'
|
|
7322
|
-
},
|
|
7323
|
-
bazelRc: {
|
|
7324
|
-
type: 'string',
|
|
7325
|
-
description: 'Path to additional .bazelrc fragments forwarded to bazel'
|
|
7326
|
-
},
|
|
7327
|
-
out: {
|
|
7328
|
-
type: 'string',
|
|
7329
|
-
description: 'Output directory for generated manifests; default: ./.socket/bazel-manifests/'
|
|
7330
|
-
},
|
|
7331
|
-
verbose: {
|
|
7332
|
-
type: 'boolean',
|
|
7333
|
-
description: 'Stream bazel stdout/stderr'
|
|
7334
|
-
}
|
|
7335
|
-
},
|
|
7336
|
-
help: (command, config) => `
|
|
7337
|
-
Usage
|
|
7338
|
-
$ ${command} [options] [CWD=.]
|
|
7339
|
-
|
|
7340
|
-
Options
|
|
7341
|
-
${utils.getFlagListOutput(config.flags)}
|
|
7342
|
-
|
|
7343
|
-
[beta] Generates Bazel JVM SBOM manifests (\`maven_install.json\`-shaped)
|
|
7344
|
-
by running \`bazel query\` against discovered Maven repos. Output is
|
|
7345
|
-
consumed by \`socket scan create\`'s server-side parser.
|
|
7346
|
-
|
|
7347
|
-
Note: this command generates Maven dependency manifests for Bazel JVM
|
|
7348
|
-
workspaces. It does not run reachability analysis.
|
|
7349
|
-
|
|
7350
|
-
To generate AND upload in one step, use \`socket scan create --auto-manifest\`
|
|
7351
|
-
instead — it detects Bazel workspaces, runs the same extraction, and uploads
|
|
7352
|
-
the result. This subcommand is for generation only.
|
|
7463
|
+
// Maximum size (bytes) we will read for any single Bazel workspace file.
|
|
7464
|
+
// Prevents DoS via maliciously large MODULE.bazel / WORKSPACE / .bzl files.
|
|
7465
|
+
const MAX_WORKSPACE_FILE_BYTES = 5 * 1024 * 1024;
|
|
7353
7466
|
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
`
|
|
7358
|
-
};
|
|
7359
|
-
const cmdManifestBazel = {
|
|
7360
|
-
description: config$e.description,
|
|
7361
|
-
hidden: config$e.hidden,
|
|
7362
|
-
run: run$F
|
|
7363
|
-
};
|
|
7364
|
-
async function run$F(argv, importMeta, {
|
|
7365
|
-
parentName
|
|
7366
|
-
}) {
|
|
7367
|
-
const cli = utils.meowOrExit({
|
|
7368
|
-
argv,
|
|
7369
|
-
config: config$e,
|
|
7370
|
-
importMeta,
|
|
7371
|
-
parentName
|
|
7372
|
-
});
|
|
7373
|
-
const {
|
|
7374
|
-
json = false,
|
|
7375
|
-
markdown = false
|
|
7376
|
-
} = cli.flags;
|
|
7377
|
-
const dryRun = !!cli.flags['dryRun'];
|
|
7467
|
+
// Maximum candidate count we will return (deduped) before failing.
|
|
7468
|
+
// Real repos have <20; this is a hard ceiling against pathological inputs.
|
|
7469
|
+
const MAX_CANDIDATES = 256;
|
|
7378
7470
|
|
|
7379
|
-
|
|
7380
|
-
const outputKind = utils.getOutputKind(json, markdown);
|
|
7381
|
-
let [cwd = '.'] = cli.input;
|
|
7382
|
-
// Note: path.resolve vs .join:
|
|
7383
|
-
// If given path is absolute then cwd should not affect it.
|
|
7384
|
-
cwd = path.resolve(process.cwd(), cwd);
|
|
7385
|
-
const sockJson = utils.readOrDefaultSocketJson(cwd);
|
|
7386
|
-
require$$9.debugFn('inspect', `override: ${constants.SOCKET_JSON} bazel`, sockJson?.defaults?.manifest?.bazel);
|
|
7387
|
-
let {
|
|
7388
|
-
bazel,
|
|
7389
|
-
bazelFlags,
|
|
7390
|
-
bazelOutputBase,
|
|
7391
|
-
bazelRc,
|
|
7392
|
-
out,
|
|
7393
|
-
verbose
|
|
7394
|
-
} = cli.flags;
|
|
7471
|
+
// Regex strategy: anchored, bounded character classes, no nested quantifiers.
|
|
7395
7472
|
|
|
7396
|
-
|
|
7397
|
-
|
|
7398
|
-
|
|
7399
|
-
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7473
|
+
// Bzlmod: discover `use_extension(..., "pip")` bindings, then match
|
|
7474
|
+
// `${binding}.parse(...)` to find pip hub declarations.
|
|
7475
|
+
// Bounded: matches up to ~256 chars of path to avoid catastrophic backtracking.
|
|
7476
|
+
const USE_EXTENSION_PIP_RE = /(\w+)\s*=\s*use_extension\s*\(\s*["'][^"']{0,256}pip\.bzl["']\s*,\s*["']pip["']\s*\)/g;
|
|
7477
|
+
|
|
7478
|
+
// Extract hub_name, requirements_lock, and python_version from a pip.parse
|
|
7479
|
+
// argument blob. Bounded character classes and length caps.
|
|
7480
|
+
const HUB_NAME_ATTR_RE = /hub_name\s*=\s*(["'])([A-Za-z0-9_]{1,129})\1/;
|
|
7481
|
+
const REQUIREMENTS_LOCK_ATTR_RE = /requirements_lock\s*=\s*(["'])([^"']{1,512})\1/;
|
|
7482
|
+
const PYTHON_VERSION_ATTR_RE = /python_version\s*=\s*(["'])([0-9._+!]{1,32})\1/;
|
|
7483
|
+
|
|
7484
|
+
// Legacy WORKSPACE patterns: pip_parse, pip_install, pip_repository.
|
|
7485
|
+
// Bounded: matches up to ~8KB of argument list.
|
|
7486
|
+
const PIP_PARSE_NAME_RE = /pip_parse\s*\(\s*([^)]{0,8192})\)/g;
|
|
7487
|
+
const PIP_INSTALL_NAME_RE = /pip_install\s*\(\s*([^)]{0,8192})\)/g;
|
|
7488
|
+
const PIP_REPOSITORY_NAME_RE = /pip_repository\s*\(\s*([^)]{0,8192})\)/g;
|
|
7489
|
+
const NAME_ATTR_RE = /name\s*=\s*(["'])([A-Za-z0-9_]{1,129})\1/;
|
|
7490
|
+
const LEGACY_REQ_LOCK_RE = /requirements_lock\s*=\s*(["'])([^"']{1,512})\1/;
|
|
7491
|
+
const MOD_SHOW_PIP_PARSE_RE = /pip\.parse\s*\(\s*([^)]{0,8192})\)/g;
|
|
7492
|
+
const MOD_SHOW_USE_REPO_RE = /use_repo\s*\(\s*\w+\s*,\s*(["'])([A-Za-z0-9_]{1,129})\1\s*\)/g;
|
|
7493
|
+
|
|
7494
|
+
// Hub validation: accept alias rules or `:pkg` targets in probe stdout.
|
|
7495
|
+
// Does NOT require `pypi_name=` (that marker lives on spoke repos).
|
|
7496
|
+
const PYPI_HUB_MARKER_RE = /:pkg\b|alias\s*\(/;
|
|
7497
|
+
function parseBazelModPipExtensionCandidates(stdout, verbose) {
|
|
7498
|
+
const useRepoNames = new Set();
|
|
7499
|
+
for (const m of stdout.matchAll(MOD_SHOW_USE_REPO_RE)) {
|
|
7500
|
+
useRepoNames.add(m[2]);
|
|
7405
7501
|
}
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
bazelFlags = '';
|
|
7502
|
+
const candidates = [];
|
|
7503
|
+
for (const m of stdout.matchAll(MOD_SHOW_PIP_PARSE_RE)) {
|
|
7504
|
+
const info = extractHubInfoFromArgBlob(m[1] ?? '', 'bazel-mod-show-extension', 'bzlmod');
|
|
7505
|
+
if (!info) {
|
|
7506
|
+
continue;
|
|
7412
7507
|
}
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7508
|
+
if (useRepoNames.size && !useRepoNames.has(info.hubName)) {
|
|
7509
|
+
if (verbose) {
|
|
7510
|
+
logger.logger.log(`[VERBOSE] discovery: dropping pip.parse hub '${info.hubName}' because show_extension did not report matching use_repo.`);
|
|
7511
|
+
}
|
|
7512
|
+
continue;
|
|
7418
7513
|
}
|
|
7514
|
+
candidates.push(info);
|
|
7419
7515
|
}
|
|
7420
|
-
if (
|
|
7421
|
-
|
|
7422
|
-
bazelRc = sockJson.defaults?.manifest?.bazel?.bazelRc;
|
|
7423
|
-
logger.logger.info(`Using default --bazel-rc from ${constants.SOCKET_JSON}:`, bazelRc);
|
|
7424
|
-
}
|
|
7516
|
+
if (verbose) {
|
|
7517
|
+
logger.logger.log('[VERBOSE] discovery: bazel mod show_extension pip.parse hits:', candidates.length, 'use_repo:', Array.from(useRepoNames));
|
|
7425
7518
|
}
|
|
7426
|
-
|
|
7427
|
-
|
|
7428
|
-
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7519
|
+
return dedupCapped(candidates, verbose);
|
|
7520
|
+
}
|
|
7521
|
+
|
|
7522
|
+
// Reads file contents, refusing files that exceed MAX_WORKSPACE_FILE_BYTES.
|
|
7523
|
+
// Returns null when the file is missing, oversized, or unreadable.
|
|
7524
|
+
function safeReadFile(file) {
|
|
7525
|
+
if (!fs$1.existsSync(file)) {
|
|
7526
|
+
return null;
|
|
7433
7527
|
}
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7437
|
-
|
|
7438
|
-
} else {
|
|
7439
|
-
verbose = false;
|
|
7528
|
+
try {
|
|
7529
|
+
const stat = fs$1.statSync(file);
|
|
7530
|
+
if (stat.size > MAX_WORKSPACE_FILE_BYTES) {
|
|
7531
|
+
return null;
|
|
7440
7532
|
}
|
|
7533
|
+
return fs$1.readFileSync(file, 'utf8');
|
|
7534
|
+
} catch {
|
|
7535
|
+
return null;
|
|
7441
7536
|
}
|
|
7442
|
-
|
|
7443
|
-
|
|
7444
|
-
|
|
7445
|
-
|
|
7446
|
-
|
|
7447
|
-
|
|
7537
|
+
}
|
|
7538
|
+
|
|
7539
|
+
// Walks workspace root for legacy Starlark sources we can scan: WORKSPACE
|
|
7540
|
+
// (and WORKSPACE.bazel) plus top-level .bzl files. Non-recursive by design;
|
|
7541
|
+
// Phase 1 explicitly avoids static Starlark parsing at depth.
|
|
7542
|
+
function listLegacyStarlarkFiles(cwd) {
|
|
7543
|
+
const files = [];
|
|
7544
|
+
const candidates = ['WORKSPACE', 'WORKSPACE.bazel'];
|
|
7545
|
+
for (const c of candidates) {
|
|
7546
|
+
const p = path.join(cwd, c);
|
|
7547
|
+
if (fs$1.existsSync(p)) {
|
|
7548
|
+
files.push(p);
|
|
7549
|
+
}
|
|
7550
|
+
}
|
|
7551
|
+
// Top-level .bzl files only.
|
|
7552
|
+
try {
|
|
7553
|
+
for (const entry of fs$1.readdirSync(cwd)) {
|
|
7554
|
+
if (entry.endsWith('.bzl')) {
|
|
7555
|
+
files.push(path.join(cwd, entry));
|
|
7556
|
+
}
|
|
7557
|
+
}
|
|
7558
|
+
} catch {
|
|
7559
|
+
// Ignore unreadable cwd.
|
|
7560
|
+
}
|
|
7561
|
+
return files;
|
|
7562
|
+
}
|
|
7563
|
+
|
|
7564
|
+
// Returns deduplicated list of items, capped at MAX_CANDIDATES.
|
|
7565
|
+
// Precedence: the first occurrence of a given hubName wins. Callers
|
|
7566
|
+
// must order inputs so the preferred source comes first (e.g., Bzlmod
|
|
7567
|
+
// hits before legacy WORKSPACE hits during migration).
|
|
7568
|
+
// Throws a clear error if the cap is exceeded so callers do not silently
|
|
7569
|
+
// truncate. Emits a verbose warning when a later entry is dropped due to
|
|
7570
|
+
// a name collision so users can see implicit precedence at work.
|
|
7571
|
+
function dedupCapped(items, verbose) {
|
|
7572
|
+
const seen = new Map();
|
|
7573
|
+
const out = [];
|
|
7574
|
+
for (const item of items) {
|
|
7575
|
+
const existing = seen.get(item.hubName);
|
|
7576
|
+
if (!existing) {
|
|
7577
|
+
seen.set(item.hubName, item);
|
|
7578
|
+
out.push(item);
|
|
7579
|
+
if (out.length >= MAX_CANDIDATES) {
|
|
7580
|
+
throw new Error(`Discovered more than ${MAX_CANDIDATES} pip hub candidates. ` + 'This exceeds the safety ceiling; aborting discovery.');
|
|
7581
|
+
}
|
|
7582
|
+
} else if (verbose) {
|
|
7583
|
+
logger.logger.log(`[VERBOSE] discovery: dropping duplicate pip hub candidate '${item.hubName}' ` + `(kept first occurrence from ${existing.source}/${existing.workspaceMode}, ` + `dropped ${item.source}/${item.workspaceMode}).`);
|
|
7584
|
+
}
|
|
7585
|
+
}
|
|
7586
|
+
return out;
|
|
7587
|
+
}
|
|
7588
|
+
|
|
7589
|
+
// Build a dynamic regex for `${binding}.parse(...)` given a validated binding
|
|
7590
|
+
// name (word characters only, so safe to embed). Bounded arg list.
|
|
7591
|
+
function buildPipParseRe(binding) {
|
|
7592
|
+
return new RegExp(`${binding}\\.parse\\s*\\(\\s*([^)]{0,8192})\\)`, 'g');
|
|
7593
|
+
}
|
|
7594
|
+
|
|
7595
|
+
// Extract candidate hub fields from a pip.parse / pip_parse / pip_install /
|
|
7596
|
+
// pip_repository argument blob (without probeStdout or visibleRepoNames).
|
|
7597
|
+
function extractHubInfoFromArgBlob(argBlob, source, workspaceMode) {
|
|
7598
|
+
const hubMatch = HUB_NAME_ATTR_RE.exec(argBlob);
|
|
7599
|
+
const nameMatch = NAME_ATTR_RE.exec(argBlob);
|
|
7600
|
+
const hubName = hubMatch?.[2] ?? nameMatch?.[2];
|
|
7601
|
+
if (!hubName) {
|
|
7602
|
+
return undefined;
|
|
7603
|
+
}
|
|
7604
|
+
const lockMatch = REQUIREMENTS_LOCK_ATTR_RE.exec(argBlob) ?? LEGACY_REQ_LOCK_RE.exec(argBlob);
|
|
7605
|
+
const pythonVersion = PYTHON_VERSION_ATTR_RE.exec(argBlob)?.[2];
|
|
7606
|
+
return {
|
|
7607
|
+
hubName,
|
|
7608
|
+
source,
|
|
7609
|
+
workspaceMode,
|
|
7610
|
+
pythonVersion,
|
|
7611
|
+
requirementsLockLabel: lockMatch?.[2]
|
|
7612
|
+
};
|
|
7613
|
+
}
|
|
7614
|
+
|
|
7615
|
+
// Step 1: parse candidate pip hub names from Bzlmod MODULE.bazel and legacy
|
|
7616
|
+
// WORKSPACE / .bzl entry points.
|
|
7617
|
+
//
|
|
7618
|
+
// Precedence: Bzlmod (MODULE.bazel pip.parse) hits are pushed first, then
|
|
7619
|
+
// legacy (pip_parse / pip_install / pip_repository) hits. dedupCapped keeps
|
|
7620
|
+
// the first occurrence, so during migration scenarios where both
|
|
7621
|
+
// MODULE.bazel and WORKSPACE define a hub with the same name, the Bzlmod
|
|
7622
|
+
// entry wins implicitly. Pass verbose=true to surface dropped duplicates.
|
|
7623
|
+
function parsePypiHubCandidates(cwd, verbose) {
|
|
7624
|
+
const candidates = [];
|
|
7625
|
+
|
|
7626
|
+
// Bzlmod path: parse MODULE.bazel for use_extension bindings to pip,
|
|
7627
|
+
// then match ${binding}.parse(...).
|
|
7628
|
+
const moduleBazel = path.join(cwd, 'MODULE.bazel');
|
|
7629
|
+
const moduleContent = safeReadFile(moduleBazel);
|
|
7630
|
+
if (moduleContent) {
|
|
7631
|
+
const bindings = [];
|
|
7632
|
+
for (const m of moduleContent.matchAll(USE_EXTENSION_PIP_RE)) {
|
|
7633
|
+
bindings.push(m[1]);
|
|
7634
|
+
}
|
|
7635
|
+
if (verbose) {
|
|
7636
|
+
logger.logger.log('[VERBOSE] discovery: scanned', moduleBazel, `(${bindings.length} use_extension pip binding(s))`);
|
|
7637
|
+
}
|
|
7638
|
+
for (const binding of bindings) {
|
|
7639
|
+
const parseRe = buildPipParseRe(binding);
|
|
7640
|
+
for (const m of moduleContent.matchAll(parseRe)) {
|
|
7641
|
+
const argBlob = m[1] ?? '';
|
|
7642
|
+
const info = extractHubInfoFromArgBlob(argBlob, 'MODULE.bazel', 'bzlmod');
|
|
7643
|
+
if (info) {
|
|
7644
|
+
candidates.push(info);
|
|
7645
|
+
}
|
|
7646
|
+
}
|
|
7647
|
+
}
|
|
7648
|
+
if (verbose) {
|
|
7649
|
+
logger.logger.log('[VERBOSE] discovery: MODULE.bazel pip.parse hits:', candidates.length);
|
|
7650
|
+
}
|
|
7651
|
+
} else if (verbose) {
|
|
7652
|
+
logger.logger.log('[VERBOSE] discovery:', moduleBazel, 'not present (skipping bzlmod scan)');
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7655
|
+
// Legacy path: scan WORKSPACE + top-level .bzl files for pip_parse,
|
|
7656
|
+
// pip_install, and pip_repository.
|
|
7657
|
+
const legacyFiles = listLegacyStarlarkFiles(cwd);
|
|
7658
|
+
if (verbose) {
|
|
7659
|
+
logger.logger.log('[VERBOSE] discovery: legacy files considered:', legacyFiles.length ? legacyFiles : '(none)');
|
|
7660
|
+
}
|
|
7661
|
+
for (const file of legacyFiles) {
|
|
7662
|
+
const content = safeReadFile(file);
|
|
7663
|
+
if (!content) {
|
|
7664
|
+
continue;
|
|
7665
|
+
}
|
|
7666
|
+
const fileHits = [];
|
|
7667
|
+
const source = file.endsWith('.bzl') ? '.bzl' : path.basename(file) === 'WORKSPACE.bazel' ? 'WORKSPACE.bazel' : 'WORKSPACE';
|
|
7668
|
+
for (const m of content.matchAll(PIP_PARSE_NAME_RE)) {
|
|
7669
|
+
const info = extractHubInfoFromArgBlob(m[1] ?? '', source, 'legacy');
|
|
7670
|
+
if (info) {
|
|
7671
|
+
fileHits.push(info);
|
|
7672
|
+
}
|
|
7673
|
+
}
|
|
7674
|
+
for (const m of content.matchAll(PIP_INSTALL_NAME_RE)) {
|
|
7675
|
+
const info = extractHubInfoFromArgBlob(m[1] ?? '', source, 'legacy');
|
|
7676
|
+
if (info) {
|
|
7677
|
+
fileHits.push(info);
|
|
7678
|
+
}
|
|
7679
|
+
}
|
|
7680
|
+
for (const m of content.matchAll(PIP_REPOSITORY_NAME_RE)) {
|
|
7681
|
+
const info = extractHubInfoFromArgBlob(m[1] ?? '', source, 'legacy');
|
|
7682
|
+
if (info) {
|
|
7683
|
+
fileHits.push(info);
|
|
7684
|
+
}
|
|
7685
|
+
}
|
|
7686
|
+
candidates.push(...fileHits);
|
|
7687
|
+
if (verbose) {
|
|
7688
|
+
logger.logger.log('[VERBOSE] discovery: scanned', file, `(${fileHits.length} legacy pip hub match(es))`);
|
|
7689
|
+
}
|
|
7690
|
+
}
|
|
7691
|
+
return dedupCapped(candidates, verbose);
|
|
7692
|
+
}
|
|
7693
|
+
|
|
7694
|
+
// Step 2: validate a candidate by running the probe and confirming
|
|
7695
|
+
// `:pkg` labels or alias rules appear in stdout. Does NOT require
|
|
7696
|
+
// `pypi_name=` (that marker lives on spoke repos).
|
|
7697
|
+
async function validatePypiHub(hubName, probe, verbose) {
|
|
7698
|
+
try {
|
|
7699
|
+
const result = await probe(hubName);
|
|
7700
|
+
if (result.code !== 0) {
|
|
7701
|
+
if (verbose) {
|
|
7702
|
+
logger.logger.log(`[VERBOSE] discovery: probe @${hubName}: REJECT (code=${result.code})`);
|
|
7703
|
+
}
|
|
7704
|
+
return {
|
|
7705
|
+
valid: false,
|
|
7706
|
+
stdout: result.stdout
|
|
7707
|
+
};
|
|
7708
|
+
}
|
|
7709
|
+
const valid = PYPI_HUB_MARKER_RE.test(result.stdout);
|
|
7710
|
+
if (verbose) {
|
|
7711
|
+
logger.logger.log(`[VERBOSE] discovery: probe @${hubName}:`, valid ? 'ACCEPT (hub alias/pkg marker found)' : 'REJECT (no hub alias/pkg marker in probe stdout)');
|
|
7712
|
+
}
|
|
7713
|
+
return {
|
|
7714
|
+
valid,
|
|
7715
|
+
stdout: result.stdout
|
|
7716
|
+
};
|
|
7717
|
+
} catch (e) {
|
|
7718
|
+
if (verbose) {
|
|
7719
|
+
logger.logger.log(`[VERBOSE] discovery: probe @${hubName}: REJECT (probe threw):`, utils.getErrorCause(e));
|
|
7720
|
+
}
|
|
7721
|
+
return {
|
|
7722
|
+
valid: false,
|
|
7723
|
+
stdout: ''
|
|
7724
|
+
};
|
|
7725
|
+
}
|
|
7726
|
+
}
|
|
7727
|
+
|
|
7728
|
+
// The default pip hub name when no explicit hub_name/name is given.
|
|
7729
|
+
// Included as a seed so repos whose pip.parse is in a sub-module (not
|
|
7730
|
+
// found by static scanning) can still be discovered via probe validation.
|
|
7731
|
+
const DEFAULT_PYPI_HUB_SEED = 'pypi';
|
|
7732
|
+
|
|
7733
|
+
// Composition: parse, then validate each candidate; return validated subset
|
|
7734
|
+
// as a Map keyed by hub name with the validated PypiHubInfo.
|
|
7735
|
+
// Always seeds with the default 'pypi' hub name first.
|
|
7736
|
+
async function discoverPypiHubs(cwd, probe, nativeCandidates, verbose, bazelCommandCandidates) {
|
|
7737
|
+
// Always run the static parse so MODULE.bazel pip.parse metadata
|
|
7738
|
+
// (requirements_lock, python_version) is available for downstream
|
|
7739
|
+
// lockfile resolution. Native repo-mapping candidates are intentionally
|
|
7740
|
+
// corroborating data only: many non-PyPI repositories expose alias or :pkg
|
|
7741
|
+
// targets, so bare visible repos are too broad to probe as PyPI hubs.
|
|
7742
|
+
const parsedAll = bazelCommandCandidates?.length ? dedupCapped(bazelCommandCandidates, verbose) : parsePypiHubCandidates(cwd, verbose);
|
|
7743
|
+
const parsed = parsedAll;
|
|
7744
|
+
if (verbose) {
|
|
7745
|
+
logger.logger.log('[VERBOSE] discovery: candidate source:', bazelCommandCandidates?.length ? `bazel mod show_extension (${parsed.length})` : nativeCandidates && nativeCandidates.length ? `static parse (${parsed.length}) with bzlmod visible-repos (${nativeCandidates.length}) as corroboration` : `static parse (${parsed.length})`);
|
|
7746
|
+
}
|
|
7747
|
+
// Prepend the default hub seed unless parsed metadata already covers it.
|
|
7748
|
+
const candidates = parsed.some(c => c.hubName === DEFAULT_PYPI_HUB_SEED) ? parsed : [{
|
|
7749
|
+
hubName: DEFAULT_PYPI_HUB_SEED,
|
|
7750
|
+
source: 'default-seed',
|
|
7751
|
+
workspaceMode: 'unknown'
|
|
7752
|
+
}, ...parsed];
|
|
7753
|
+
if (verbose) {
|
|
7754
|
+
logger.logger.log('[VERBOSE] discovery: candidate set to probe (seed-first, deduped):', candidates.map(c => c.hubName));
|
|
7755
|
+
}
|
|
7756
|
+
const validated = new Map();
|
|
7757
|
+
for (const c of candidates) {
|
|
7758
|
+
// eslint-disable-next-line no-await-in-loop
|
|
7759
|
+
const result = await validatePypiHub(c.hubName, probe, verbose);
|
|
7760
|
+
if (result.valid) {
|
|
7761
|
+
validated.set(c.hubName, {
|
|
7762
|
+
...c,
|
|
7763
|
+
probeStdout: result.stdout
|
|
7764
|
+
});
|
|
7765
|
+
}
|
|
7766
|
+
}
|
|
7767
|
+
if (verbose) {
|
|
7768
|
+
logger.logger.log('[VERBOSE] discovery: validated pip hubs:', Array.from(validated.keys()));
|
|
7769
|
+
}
|
|
7770
|
+
return validated;
|
|
7771
|
+
}
|
|
7772
|
+
|
|
7773
|
+
/**
|
|
7774
|
+
* Parse Bazel PyPI extraction inputs into the pinned `name==version` lines
|
|
7775
|
+
* needed for generated `requirements.txt` output.
|
|
7776
|
+
*
|
|
7777
|
+
* This is deliberately not a general-purpose requirements.txt parser. It only
|
|
7778
|
+
* accepts pinned lockfile-style entries needed to map reached Bazel labels to
|
|
7779
|
+
* exact package versions; depscan remains the owner of full PEP 508
|
|
7780
|
+
* requirements ingestion during scan processing.
|
|
7781
|
+
*
|
|
7782
|
+
* Security gate: every regex uses bounded character classes to prevent
|
|
7783
|
+
* catastrophic backtracking on hostile input.
|
|
7784
|
+
*/
|
|
7785
|
+
|
|
7786
|
+
|
|
7787
|
+
// Maximum size (bytes) we will read for any requirements lockfile.
|
|
7788
|
+
// Prevents DoS via maliciously large lockfiles.
|
|
7789
|
+
const MAX_REQUIREMENTS_FILE_BYTES = 5 * 1024 * 1024;
|
|
7790
|
+
// Normalize a PyPI package name per PEP 503:
|
|
7791
|
+
// lowercase, then collapse `.`, `_`, and `-` runs to a single `-`.
|
|
7792
|
+
function normalizePypiName(name) {
|
|
7793
|
+
return name.toLowerCase().replace(/[._-]+/g, '-').replace(/^-+/, '').replace(/-+$/, '');
|
|
7794
|
+
}
|
|
7795
|
+
|
|
7796
|
+
// Convert a Bazel underscore_name to a PyPI hyphenated-name.
|
|
7797
|
+
function bazelNameToPypiName(bazelName) {
|
|
7798
|
+
return bazelName.replace(/_/g, '-');
|
|
7799
|
+
}
|
|
7800
|
+
|
|
7801
|
+
// Validate that a resolved path stays within the workspace root.
|
|
7802
|
+
function isWithinWorkspace(resolved, cwd) {
|
|
7803
|
+
const rel = path.relative(cwd, resolved);
|
|
7804
|
+
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
7805
|
+
}
|
|
7806
|
+
|
|
7807
|
+
// Resolves a Bazel label or workspace-relative path to a filesystem path.
|
|
7808
|
+
// Returns undefined for labels that cannot be resolved locally.
|
|
7809
|
+
function resolveRequirementsLockPath(label, cwd) {
|
|
7810
|
+
if (!label) {
|
|
7811
|
+
return undefined;
|
|
7812
|
+
}
|
|
7813
|
+
// Reject labels with path-traversal segments.
|
|
7814
|
+
if (label.includes('..')) {
|
|
7815
|
+
return undefined;
|
|
7816
|
+
}
|
|
7817
|
+
// Reject external repository labels.
|
|
7818
|
+
if (label.startsWith('@')) {
|
|
7819
|
+
return undefined;
|
|
7820
|
+
}
|
|
7821
|
+
// Bazel local label forms:
|
|
7822
|
+
// //:requirements_lock.txt
|
|
7823
|
+
// //subdir:requirements_lock.txt
|
|
7824
|
+
// :requirements_lock.txt
|
|
7825
|
+
let filePart;
|
|
7826
|
+
if (label.startsWith('//')) {
|
|
7827
|
+
const colon = label.indexOf(':');
|
|
7828
|
+
if (colon < 0) {
|
|
7829
|
+
return undefined;
|
|
7830
|
+
}
|
|
7831
|
+
const pkgPath = label.slice(2, colon);
|
|
7832
|
+
const filePart = label.slice(colon + 1);
|
|
7833
|
+
if (!filePart) {
|
|
7834
|
+
return undefined;
|
|
7835
|
+
}
|
|
7836
|
+
const resolved = path.join(cwd, pkgPath, filePart);
|
|
7837
|
+
if (!isWithinWorkspace(resolved, cwd)) {
|
|
7838
|
+
return undefined;
|
|
7839
|
+
}
|
|
7840
|
+
return resolved;
|
|
7841
|
+
}
|
|
7842
|
+
if (label.startsWith(':')) {
|
|
7843
|
+
filePart = label.slice(1);
|
|
7844
|
+
if (!filePart) {
|
|
7845
|
+
return undefined;
|
|
7846
|
+
}
|
|
7847
|
+
const resolved = path.join(cwd, filePart);
|
|
7848
|
+
if (!isWithinWorkspace(resolved, cwd)) {
|
|
7849
|
+
return undefined;
|
|
7850
|
+
}
|
|
7851
|
+
return resolved;
|
|
7852
|
+
}
|
|
7853
|
+
// Reject absolute paths (only for non-label inputs).
|
|
7854
|
+
if (path.isAbsolute(label)) {
|
|
7855
|
+
return undefined;
|
|
7856
|
+
}
|
|
7857
|
+
// Bare workspace-relative path (no leading // or :).
|
|
7858
|
+
const resolved = path.join(cwd, label);
|
|
7859
|
+
if (!isWithinWorkspace(resolved, cwd)) {
|
|
7860
|
+
return undefined;
|
|
7861
|
+
}
|
|
7862
|
+
return resolved;
|
|
7863
|
+
}
|
|
7864
|
+
|
|
7865
|
+
// Parses a single pinned `name==version` lockfile line.
|
|
7866
|
+
// Group 1 = package name, Group 2 = version string (includes ==).
|
|
7867
|
+
const REQUIREMENT_LINE_RE = /^([A-Za-z0-9][A-Za-z0-9._-]*)==([A-Za-z0-9._+!]+)/;
|
|
7868
|
+
const BAZEL_STRING_LABEL_RE = /[@A-Za-z0-9_~/.:+-]+/;
|
|
7869
|
+
const ALIAS_ACTUAL_RE = new RegExp(`actual\\s*=\\s*(["'])(${BAZEL_STRING_LABEL_RE.source})\\1`);
|
|
7870
|
+
|
|
7871
|
+
// Skippable line prefixes.
|
|
7872
|
+
function shouldSkipLine(line) {
|
|
7873
|
+
const trimmed = line.trim();
|
|
7874
|
+
if (!trimmed) {
|
|
7875
|
+
return true;
|
|
7876
|
+
}
|
|
7877
|
+
if (trimmed.startsWith('#')) {
|
|
7878
|
+
return true;
|
|
7879
|
+
}
|
|
7880
|
+
// Hash continuations start with `--hash=`.
|
|
7881
|
+
if (trimmed.startsWith('--hash=')) {
|
|
7882
|
+
return true;
|
|
7883
|
+
}
|
|
7884
|
+
// Index options, constraint options, editable installs, includes, direct URLs.
|
|
7885
|
+
if (trimmed.startsWith('--') || trimmed.startsWith('-e ') || trimmed.startsWith('-r ') || trimmed.startsWith('https://') || trimmed.startsWith('http://')) {
|
|
7886
|
+
return true;
|
|
7887
|
+
}
|
|
7888
|
+
return false;
|
|
7889
|
+
}
|
|
7890
|
+
|
|
7891
|
+
// Parse a `requirements_lock.txt`-style file into a map keyed by normalized
|
|
7892
|
+
// PyPI name. This intentionally ignores unpinned PEP 508 requirement forms
|
|
7893
|
+
// because the Bazel extractor must emit exact package versions.
|
|
7894
|
+
function parseRequirementsLock(text) {
|
|
7895
|
+
const out = new Map();
|
|
7896
|
+
const lines = text.split('\n');
|
|
7897
|
+
for (let i = 0; i < lines.length; i++) {
|
|
7898
|
+
const rawLine = lines[i];
|
|
7899
|
+
if (rawLine === undefined) {
|
|
7900
|
+
continue;
|
|
7901
|
+
}
|
|
7902
|
+
if (shouldSkipLine(rawLine)) {
|
|
7903
|
+
continue;
|
|
7904
|
+
}
|
|
7905
|
+
// Handle trailing backslash continuation by concatenating subsequent lines.
|
|
7906
|
+
let line = rawLine.trimEnd();
|
|
7907
|
+
while (line.endsWith('\\') && i + 1 < lines.length) {
|
|
7908
|
+
i++;
|
|
7909
|
+
const next = lines[i];
|
|
7910
|
+
if (next !== undefined) {
|
|
7911
|
+
line = line.slice(0, -1).trimEnd() + ' ' + next.trimStart();
|
|
7912
|
+
}
|
|
7913
|
+
}
|
|
7914
|
+
const m = REQUIREMENT_LINE_RE.exec(line);
|
|
7915
|
+
if (!m) {
|
|
7916
|
+
continue;
|
|
7917
|
+
}
|
|
7918
|
+
const [, rawName, version] = m;
|
|
7919
|
+
if (!rawName || !version) {
|
|
7920
|
+
continue;
|
|
7921
|
+
}
|
|
7922
|
+
const bazelName = rawName.replace(/-/g, '_');
|
|
7923
|
+
const normalized = normalizePypiName(rawName);
|
|
7924
|
+
const existing = out.get(normalized);
|
|
7925
|
+
if (existing) {
|
|
7926
|
+
if (existing.version !== version) {
|
|
7927
|
+
throw new Error(`Conflicting versions for normalized PyPI package ${normalized}: ` + `${existing.originalLine ?? existing.name + '==' + existing.version} ` + `conflicts with ${line}.`);
|
|
7928
|
+
}
|
|
7929
|
+
continue;
|
|
7930
|
+
}
|
|
7931
|
+
out.set(normalized, {
|
|
7932
|
+
name: rawName,
|
|
7933
|
+
version,
|
|
7934
|
+
bazelName,
|
|
7935
|
+
source: 'lockfile',
|
|
7936
|
+
originalLine: line
|
|
7937
|
+
});
|
|
7938
|
+
}
|
|
7939
|
+
return out;
|
|
7940
|
+
}
|
|
7941
|
+
|
|
7942
|
+
// Read and parse a requirements lockfile from a resolved path, capping file
|
|
7943
|
+
// size. Returns undefined when the file is missing, oversized, or unreadable.
|
|
7944
|
+
function readRequirementsLockFile(resolvedPath) {
|
|
7945
|
+
if (!resolvedPath) {
|
|
7946
|
+
return undefined;
|
|
7947
|
+
}
|
|
7948
|
+
if (!fs$1.existsSync(resolvedPath)) {
|
|
7949
|
+
return undefined;
|
|
7950
|
+
}
|
|
7951
|
+
let text;
|
|
7952
|
+
try {
|
|
7953
|
+
const stat = fs$1.statSync(resolvedPath);
|
|
7954
|
+
if (stat.size > MAX_REQUIREMENTS_FILE_BYTES) {
|
|
7955
|
+
return undefined;
|
|
7956
|
+
}
|
|
7957
|
+
text = fs$1.readFileSync(resolvedPath, 'utf8');
|
|
7958
|
+
} catch {
|
|
7959
|
+
return undefined;
|
|
7960
|
+
}
|
|
7961
|
+
return parseRequirementsLock(text);
|
|
7962
|
+
}
|
|
7963
|
+
|
|
7964
|
+
// Extract `pypi_name=` and `pypi_version=` tags from `--output=build` text of a
|
|
7965
|
+
// spoke target. Returns null when either tag is missing.
|
|
7966
|
+
const PYPI_NAME_TAG_RE = /pypi_name=\s*([A-Za-z0-9][A-Za-z0-9._-]*)/;
|
|
7967
|
+
const PYPI_VERSION_TAG_RE = /pypi_version=\s*([A-Za-z0-9._+!]+)/;
|
|
7968
|
+
function parsePypiTagsFromBuildOutput(text) {
|
|
7969
|
+
const nameM = PYPI_NAME_TAG_RE.exec(text);
|
|
7970
|
+
const versionM = PYPI_VERSION_TAG_RE.exec(text);
|
|
7971
|
+
if (!nameM || !versionM) {
|
|
7972
|
+
return null;
|
|
7973
|
+
}
|
|
7974
|
+
const rawName = nameM[1];
|
|
7975
|
+
const version = versionM[1];
|
|
7976
|
+
if (!rawName || !version) {
|
|
7977
|
+
return null;
|
|
7978
|
+
}
|
|
7979
|
+
return {
|
|
7980
|
+
name: rawName,
|
|
7981
|
+
version,
|
|
7982
|
+
bazelName: rawName.replace(/-/g, '_'),
|
|
7983
|
+
source: 'spoke-tag'
|
|
7984
|
+
};
|
|
7985
|
+
}
|
|
7986
|
+
function parseAliasActualFromBuildOutput(text) {
|
|
7987
|
+
const match = ALIAS_ACTUAL_RE.exec(text);
|
|
7988
|
+
return match?.[2];
|
|
7989
|
+
}
|
|
7990
|
+
|
|
7991
|
+
// Extract hub package labels from `bazel query` output that match
|
|
7992
|
+
// `@<hub>//<name>:pkg` patterns (both line-start and embedded in
|
|
7993
|
+
// `--output=build` deps arrays).
|
|
7994
|
+
function filterReachedPypiPackages(queryOutput, hubName) {
|
|
7995
|
+
const out = [];
|
|
7996
|
+
const prefix = `@${hubName}//`;
|
|
7997
|
+
// Match from the start of a label token (preceded by whitespace, quote, or
|
|
7998
|
+
// start of line) to improve robustness across output formats.
|
|
7999
|
+
const labelRe = new RegExp(`(?:^|[\\s"])${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^\\s:"]+):pkg`, 'g');
|
|
8000
|
+
let m;
|
|
8001
|
+
while ((m = labelRe.exec(queryOutput)) !== null) {
|
|
8002
|
+
const pkgPart = m[1];
|
|
8003
|
+
if (!pkgPart) {
|
|
8004
|
+
continue;
|
|
8005
|
+
}
|
|
8006
|
+
const bazelName = pkgPart;
|
|
8007
|
+
const normalized = normalizePypiName(bazelNameToPypiName(bazelName));
|
|
8008
|
+
const apparentLabel = `${prefix}${bazelName}:pkg`;
|
|
8009
|
+
out.push({
|
|
8010
|
+
hubName,
|
|
8011
|
+
originalLabel: apparentLabel,
|
|
8012
|
+
bazelName,
|
|
8013
|
+
normalizedName: normalized,
|
|
8014
|
+
apparentLabel
|
|
8015
|
+
});
|
|
8016
|
+
}
|
|
8017
|
+
return out;
|
|
8018
|
+
}
|
|
8019
|
+
|
|
8020
|
+
// Collect name==version pairs for the reached closure, resolving versions
|
|
8021
|
+
// from the lockfile fast path or spoke-tag fallback. Enforces version
|
|
8022
|
+
// conflict detection and deterministic output.
|
|
8023
|
+
function collectPypiPackages(reached, lockfile, spokeTagLookup) {
|
|
8024
|
+
const collected = new Map();
|
|
8025
|
+
for (const r of reached) {
|
|
8026
|
+
const normalized = r.normalizedName;
|
|
8027
|
+
// Lockfile fast path.
|
|
8028
|
+
const lockEntry = lockfile?.get(normalized);
|
|
8029
|
+
if (lockEntry) {
|
|
8030
|
+
const existing = collected.get(normalized);
|
|
8031
|
+
if (existing && existing.version !== lockEntry.version) {
|
|
8032
|
+
throw new Error(`Conflicting versions for ${normalized}: ${existing.label} has ${existing.version}, ${r.originalLabel} has ${lockEntry.version} (lockfile).`);
|
|
8033
|
+
}
|
|
8034
|
+
if (!existing) {
|
|
8035
|
+
collected.set(normalized, {
|
|
8036
|
+
name: lockEntry.name,
|
|
8037
|
+
version: lockEntry.version,
|
|
8038
|
+
source: 'lockfile',
|
|
8039
|
+
label: r.originalLabel
|
|
8040
|
+
});
|
|
8041
|
+
}
|
|
8042
|
+
continue;
|
|
8043
|
+
}
|
|
8044
|
+
// Spoke-tag fallback.
|
|
8045
|
+
const spokeEntry = spokeTagLookup?.get(normalized);
|
|
8046
|
+
if (spokeEntry) {
|
|
8047
|
+
const existing = collected.get(normalized);
|
|
8048
|
+
if (existing && existing.version !== spokeEntry.version) {
|
|
8049
|
+
throw new Error(`Conflicting versions for ${normalized}: ${existing.label} has ${existing.version}, ${r.originalLabel} has ${spokeEntry.version} (spoke tag).`);
|
|
8050
|
+
}
|
|
8051
|
+
if (!existing) {
|
|
8052
|
+
collected.set(normalized, {
|
|
8053
|
+
name: spokeEntry.name,
|
|
8054
|
+
version: spokeEntry.version,
|
|
8055
|
+
source: 'spoke-tag',
|
|
8056
|
+
label: r.originalLabel
|
|
8057
|
+
});
|
|
8058
|
+
}
|
|
8059
|
+
continue;
|
|
8060
|
+
}
|
|
8061
|
+
// Unresolvable package — fail rather than emit an unpinned entry.
|
|
8062
|
+
throw new Error(`No version found for ${r.originalLabel}. ` + 'Check that the package is present in the requirements_lock.txt ' + 'or reachable via a spoke target with pypi_name and pypi_version tags.');
|
|
8063
|
+
}
|
|
8064
|
+
return Array.from(collected.values());
|
|
8065
|
+
}
|
|
8066
|
+
|
|
8067
|
+
// Sort package lines deterministically (locale-aware, lowercase comparison).
|
|
8068
|
+
function sortPackageLines(lines) {
|
|
8069
|
+
return lines.sort((a, b) => {
|
|
8070
|
+
const aLow = a.name.toLowerCase();
|
|
8071
|
+
const bLow = b.name.toLowerCase();
|
|
8072
|
+
if (aLow < bLow) {
|
|
8073
|
+
return -1;
|
|
8074
|
+
}
|
|
8075
|
+
if (aLow > bLow) {
|
|
8076
|
+
return 1;
|
|
8077
|
+
}
|
|
8078
|
+
return a.name.localeCompare(b.name);
|
|
8079
|
+
});
|
|
8080
|
+
}
|
|
8081
|
+
async function extractBazelToPypi(opts) {
|
|
8082
|
+
const {
|
|
8083
|
+
cwd,
|
|
8084
|
+
out,
|
|
8085
|
+
verbose
|
|
8086
|
+
} = opts;
|
|
8087
|
+
logger.logger.group('bazel2pypi:');
|
|
8088
|
+
logger.logger.info(`- src dir: \`${cwd}\``);
|
|
8089
|
+
logger.logger.info(`- out dir: \`${out}\``);
|
|
8090
|
+
if (!fs$1.existsSync(cwd)) {
|
|
8091
|
+
logger.logger.warn(`Warning: cwd does not exist: ${cwd}`);
|
|
8092
|
+
}
|
|
8093
|
+
logger.logger.groupEnd();
|
|
8094
|
+
try {
|
|
8095
|
+
// Validate caller-provided Bazel filesystem settings before invoking Bazel.
|
|
8096
|
+
if (opts.bazelOutputBase) {
|
|
8097
|
+
validateOutputBase(opts.bazelOutputBase, opts.cwd);
|
|
8098
|
+
}
|
|
8099
|
+
// Python shim (for rules_python workspace discovery).
|
|
8100
|
+
const shim = await provisionPythonShim();
|
|
8101
|
+
const baseEnv = shim.augmentedEnv ?? opts.env;
|
|
8102
|
+
|
|
8103
|
+
// Step 1: workspace detection.
|
|
8104
|
+
const mode = detectWorkspaceMode(cwd);
|
|
8105
|
+
logger.logger.info(`Workspace mode: bzlmod=${mode.bzlmod} workspace=${mode.workspace}`);
|
|
8106
|
+
const invocationFlags = getBazelInvocationFlags(mode);
|
|
8107
|
+
|
|
8108
|
+
// Step 2: bazel binary resolution.
|
|
8109
|
+
const bin = await resolveBazelBinary(opts.bin);
|
|
8110
|
+
logger.logger.info(`Using bazel: ${bin}`);
|
|
8111
|
+
if (verbose) {
|
|
8112
|
+
logger.logger.log('[VERBOSE] resolved options:', {
|
|
8113
|
+
bin,
|
|
8114
|
+
bazelRc: opts.bazelRc ?? '(unset)',
|
|
8115
|
+
bazelOutputBase: opts.bazelOutputBase ?? '(unset)',
|
|
8116
|
+
bazelFlags: opts.bazelFlags ?? '(unset)',
|
|
8117
|
+
invocationFlags
|
|
8118
|
+
});
|
|
8119
|
+
}
|
|
8120
|
+
|
|
8121
|
+
// Step 3: build the shared query options object.
|
|
8122
|
+
const queryOpts = {
|
|
8123
|
+
bin,
|
|
8124
|
+
cwd,
|
|
8125
|
+
invocationFlags,
|
|
8126
|
+
...(opts.bazelRc ? {
|
|
8127
|
+
bazelRc: opts.bazelRc
|
|
8128
|
+
} : {}),
|
|
8129
|
+
...(opts.bazelFlags ? {
|
|
8130
|
+
bazelFlags: opts.bazelFlags
|
|
8131
|
+
} : {}),
|
|
8132
|
+
...(opts.bazelOutputBase ? {
|
|
8133
|
+
bazelOutputBase: opts.bazelOutputBase
|
|
8134
|
+
} : {}),
|
|
8135
|
+
...(baseEnv ? {
|
|
8136
|
+
env: baseEnv
|
|
8137
|
+
} : {}),
|
|
8138
|
+
verbose
|
|
8139
|
+
};
|
|
8140
|
+
|
|
8141
|
+
// Step 4: discover validated PyPI hubs via the two-step recipe.
|
|
8142
|
+
let bazelCommandCandidates;
|
|
8143
|
+
let nativeCandidates;
|
|
8144
|
+
if (mode.bzlmod) {
|
|
8145
|
+
const extensionResult = await runBazelModShowPipExtension(queryOpts);
|
|
8146
|
+
if (extensionResult.code === 0) {
|
|
8147
|
+
bazelCommandCandidates = parseBazelModPipExtensionCandidates(extensionResult.stdout, verbose);
|
|
8148
|
+
} else if (verbose) {
|
|
8149
|
+
logger.logger.log('[VERBOSE] bazel mod show_extension failed; falling back to bounded static candidate parsing:', extensionResult.stderr);
|
|
8150
|
+
}
|
|
8151
|
+
const visibleRepos = await runBazelModShowVisibleRepos(queryOpts);
|
|
8152
|
+
if (visibleRepos.code === 0) {
|
|
8153
|
+
nativeCandidates = parseVisibleRepoCandidates(visibleRepos.stdout);
|
|
8154
|
+
if (verbose) {
|
|
8155
|
+
logger.logger.log('[VERBOSE] Bzlmod visible repo candidates:', nativeCandidates);
|
|
8156
|
+
}
|
|
8157
|
+
} else if (verbose) {
|
|
8158
|
+
logger.logger.log('[VERBOSE] bazel mod show_repo failed; falling back to static candidate parsing:', visibleRepos.stderr);
|
|
8159
|
+
}
|
|
8160
|
+
}
|
|
8161
|
+
const probe = buildPypiProbeFor(queryOpts);
|
|
8162
|
+
const hubs = await discoverPypiHubs(cwd, probe, nativeCandidates, verbose, bazelCommandCandidates);
|
|
8163
|
+
const hubNames = Array.from(hubs.keys());
|
|
8164
|
+
logger.logger.info(`Discovered ${hubs.size} PyPI hub(s): ${hubNames.join(', ') || '(none)'}`);
|
|
8165
|
+
if (!hubs.size) {
|
|
8166
|
+
if (verbose) {
|
|
8167
|
+
logger.logger.info('No PyPI hubs discovered. failureCategory=no-supported-ecosystem');
|
|
8168
|
+
}
|
|
8169
|
+
return {
|
|
8170
|
+
artifactCount: 0,
|
|
8171
|
+
ok: false,
|
|
8172
|
+
noEcosystemFound: true
|
|
8173
|
+
};
|
|
8174
|
+
}
|
|
8175
|
+
|
|
8176
|
+
// Step 5: for each hub, resolve the requirements lockfile (fast path),
|
|
8177
|
+
// run the reached-closure query, and collect name==version pairs.
|
|
8178
|
+
const allLines = [];
|
|
8179
|
+
const warnings = [];
|
|
8180
|
+
for (const [hubName, hubInfo] of hubs) {
|
|
8181
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8182
|
+
const lockfileMap = await resolveHubLockfile(hubInfo, cwd, verbose);
|
|
8183
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8184
|
+
const reached = await queryReachedPypiLabels(hubName, queryOpts, verbose);
|
|
8185
|
+
const labelsToQuery = lockfileMap ? reached.filter(label => !lockfileMap.has(label.normalizedName)) : reached;
|
|
8186
|
+
const divergenceLabels = lockfileMap && verbose ? reached : labelsToQuery;
|
|
8187
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8188
|
+
const spokeTagLookup = await buildSpokeTagLookup(divergenceLabels, queryOpts, verbose);
|
|
8189
|
+
|
|
8190
|
+
// Check for lockfile-vs-spoke-tag divergence and log warnings.
|
|
8191
|
+
if (lockfileMap) {
|
|
8192
|
+
for (const label of reached) {
|
|
8193
|
+
const lockEntry = lockfileMap.get(label.normalizedName);
|
|
8194
|
+
const spokeEntry = spokeTagLookup?.get(label.normalizedName);
|
|
8195
|
+
if (lockEntry && spokeEntry && lockEntry.version !== spokeEntry.version) {
|
|
8196
|
+
warnings.push(`Version divergence for ${label.originalLabel}: lockfile says ${lockEntry.version}, spoke tag says ${spokeEntry.version}. Using lockfile.`);
|
|
8197
|
+
}
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
8200
|
+
const lines = collectPypiPackages(reached, lockfileMap, spokeTagLookup);
|
|
8201
|
+
for (const l of lines) {
|
|
8202
|
+
allLines.push({
|
|
8203
|
+
name: l.name,
|
|
8204
|
+
version: l.version,
|
|
8205
|
+
source: l.source
|
|
8206
|
+
});
|
|
8207
|
+
}
|
|
8208
|
+
logger.logger.info(`@${hubName}: ${lines.length} package(s)`);
|
|
8209
|
+
}
|
|
8210
|
+
|
|
8211
|
+
// Step 6: cross-hub conflict check (same normalized name, different
|
|
8212
|
+
// version across multiple hubs).
|
|
8213
|
+
const crossHubVersions = new Map();
|
|
8214
|
+
for (const l of allLines) {
|
|
8215
|
+
const normalized = normalizePypiName(l.name);
|
|
8216
|
+
const existing = crossHubVersions.get(normalized);
|
|
8217
|
+
if (existing && existing !== l.version) {
|
|
8218
|
+
throw new Error(`Conflicting versions for ${l.name}: ${existing} vs ${l.version} across hubs.`);
|
|
8219
|
+
}
|
|
8220
|
+
crossHubVersions.set(normalized, l.version);
|
|
8221
|
+
}
|
|
8222
|
+
|
|
8223
|
+
// Step 7: sort and write requirements.txt.
|
|
8224
|
+
const sorted = sortPackageLines(allLines);
|
|
8225
|
+
const lines = sorted.map(p => `${p.name}==${p.version}\n`);
|
|
8226
|
+
const layout = opts.outLayout ?? 'standalone';
|
|
8227
|
+
const manifestDir = layout === 'flat' ? path.join(out, '.socket-auto-manifest') : out;
|
|
8228
|
+
fs$1.mkdirSync(manifestDir, {
|
|
8229
|
+
recursive: true
|
|
8230
|
+
});
|
|
8231
|
+
const manifestPath = path.join(manifestDir, 'requirements.txt');
|
|
8232
|
+
await fs$1.promises.writeFile(manifestPath, lines.join(''), 'utf8');
|
|
8233
|
+
if (verbose) {
|
|
8234
|
+
logger.logger.log('[VERBOSE] outputs:', {
|
|
8235
|
+
artifactCount: allLines.length,
|
|
8236
|
+
generatedManifest: path.relative(out, manifestPath),
|
|
8237
|
+
layout,
|
|
8238
|
+
manifest: manifestPath,
|
|
8239
|
+
pypiHubs: hubNames,
|
|
8240
|
+
tool: 'socket manifest bazel',
|
|
8241
|
+
workspace: {
|
|
8242
|
+
bzlmod: mode.bzlmod,
|
|
8243
|
+
legacyWorkspace: mode.workspace
|
|
8244
|
+
}
|
|
8245
|
+
});
|
|
8246
|
+
}
|
|
8247
|
+
for (const w of warnings) {
|
|
8248
|
+
logger.logger.warn(w);
|
|
8249
|
+
}
|
|
8250
|
+
if (!allLines.length) {
|
|
8251
|
+
logger.logger.fail('No PyPI packages extracted. failureCategory=ecosystem-detected-but-empty. See warnings above.');
|
|
8252
|
+
return {
|
|
8253
|
+
artifactCount: 0,
|
|
8254
|
+
manifestPath,
|
|
8255
|
+
ok: false
|
|
8256
|
+
};
|
|
8257
|
+
}
|
|
8258
|
+
logger.logger.success(`Wrote ${allLines.length} package(s) to ${path.relative(cwd, manifestPath)}.`);
|
|
8259
|
+
return {
|
|
8260
|
+
artifactCount: allLines.length,
|
|
8261
|
+
manifestPath,
|
|
8262
|
+
ok: true
|
|
8263
|
+
};
|
|
8264
|
+
} catch (e) {
|
|
8265
|
+
logger.logger.fail(`Unexpected error in bazel2pypi: ${utils.getErrorCause(e)}`);
|
|
8266
|
+
if (verbose) {
|
|
8267
|
+
logger.logger.group('[VERBOSE] error:');
|
|
8268
|
+
logger.logger.log(e);
|
|
8269
|
+
logger.logger.groupEnd();
|
|
8270
|
+
} else {
|
|
8271
|
+
logger.logger.info('Re-run with --verbose for the full stack.');
|
|
8272
|
+
}
|
|
8273
|
+
return {
|
|
8274
|
+
artifactCount: 0,
|
|
8275
|
+
ok: false
|
|
8276
|
+
};
|
|
8277
|
+
}
|
|
8278
|
+
}
|
|
8279
|
+
|
|
8280
|
+
// Resolve lockfile path and read/parse if within bounds.
|
|
8281
|
+
async function resolveHubLockfile(hubInfo, cwd, verbose) {
|
|
8282
|
+
const resolved = hubInfo.requirementsLockPath ?? resolveRequirementsLockPath(hubInfo.requirementsLockLabel, cwd);
|
|
8283
|
+
if (verbose) {
|
|
8284
|
+
logger.logger.log('[VERBOSE] lockfile resolved:', resolved ?? '(none from label/path)');
|
|
8285
|
+
}
|
|
8286
|
+
const result = readRequirementsLockFile(resolved);
|
|
8287
|
+
if (verbose && result) {
|
|
8288
|
+
logger.logger.log('[VERBOSE] lockfile parsed:', result.size, 'package(s)');
|
|
8289
|
+
}
|
|
8290
|
+
return result;
|
|
8291
|
+
}
|
|
8292
|
+
|
|
8293
|
+
// Run the reached-closure query for Python targets and filter to hub labels.
|
|
8294
|
+
async function queryReachedPypiLabels(hubName, queryOpts, verbose) {
|
|
8295
|
+
const queryStr = 'deps(kind("py_library|py_binary|py_test", //...))';
|
|
8296
|
+
const result = await runBazelQuery(queryStr, queryOpts, 'label');
|
|
8297
|
+
if (result.code !== 0) {
|
|
8298
|
+
if (verbose) {
|
|
8299
|
+
logger.logger.log(`[VERBOSE] reached query failed for ${hubName}:`, result.stderr);
|
|
8300
|
+
}
|
|
8301
|
+
return [];
|
|
8302
|
+
}
|
|
8303
|
+
return filterReachedPypiPackages(result.stdout, hubName);
|
|
8304
|
+
}
|
|
8305
|
+
|
|
8306
|
+
// Build a spoke-tag lookup map for reached labels that don't have lockfile
|
|
8307
|
+
// entries. For each reached label, if the lockfile missed it, resolve the
|
|
8308
|
+
// actual target via `--output=build` and extract pypi_name/pypi_version.
|
|
8309
|
+
async function buildSpokeTagLookup(reached, queryOpts, verbose) {
|
|
8310
|
+
const lookup = new Map();
|
|
8311
|
+
for (const label of reached) {
|
|
8312
|
+
// Only query the spoke if we haven't already resolved it.
|
|
8313
|
+
if (lookup.has(label.normalizedName)) {
|
|
8314
|
+
continue;
|
|
8315
|
+
}
|
|
8316
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8317
|
+
const buildResult = await runBazelQuery(`${label.apparentLabel}`, {
|
|
8318
|
+
...queryOpts,
|
|
8319
|
+
verbose: false
|
|
8320
|
+
});
|
|
8321
|
+
if (buildResult.code !== 0) {
|
|
8322
|
+
if (verbose) {
|
|
8323
|
+
logger.logger.log(`[VERBOSE] spoke build query failed for ${label.apparentLabel}:`, buildResult.stderr);
|
|
8324
|
+
}
|
|
8325
|
+
continue;
|
|
8326
|
+
}
|
|
8327
|
+
let parsed = parsePypiTagsFromBuildOutput(buildResult.stdout);
|
|
8328
|
+
if (!parsed) {
|
|
8329
|
+
const actualLabel = parseAliasActualFromBuildOutput(buildResult.stdout);
|
|
8330
|
+
if (actualLabel && actualLabel !== label.apparentLabel) {
|
|
8331
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8332
|
+
const actualResult = await runBazelQuery(actualLabel, {
|
|
8333
|
+
...queryOpts,
|
|
8334
|
+
verbose: false
|
|
8335
|
+
});
|
|
8336
|
+
if (actualResult.code === 0) {
|
|
8337
|
+
parsed = parsePypiTagsFromBuildOutput(actualResult.stdout);
|
|
8338
|
+
} else if (verbose) {
|
|
8339
|
+
logger.logger.log(`[VERBOSE] spoke actual query failed for ${actualLabel}:`, actualResult.stderr);
|
|
8340
|
+
}
|
|
8341
|
+
}
|
|
8342
|
+
}
|
|
8343
|
+
if (parsed) {
|
|
8344
|
+
lookup.set(normalizePypiName(parsed.name), parsed);
|
|
8345
|
+
}
|
|
8346
|
+
}
|
|
8347
|
+
return lookup;
|
|
8348
|
+
}
|
|
8349
|
+
|
|
8350
|
+
const config$e = {
|
|
8351
|
+
commandName: 'bazel',
|
|
8352
|
+
description: '[beta] Bazel SBOM support — generate manifest files for a Bazel project (Maven, PyPI)',
|
|
8353
|
+
hidden: false,
|
|
8354
|
+
flags: {
|
|
8355
|
+
...flags.commonFlags,
|
|
8356
|
+
bazel: {
|
|
8357
|
+
type: 'string',
|
|
8358
|
+
description: 'Path to bazel/bazelisk binary; default: $(which bazelisk) || $(which bazel)'
|
|
8359
|
+
},
|
|
8360
|
+
bazelFlags: {
|
|
8361
|
+
type: 'string',
|
|
8362
|
+
description: 'Flags forwarded to every bazel invocation (single quoted string)'
|
|
8363
|
+
},
|
|
8364
|
+
bazelOutputBase: {
|
|
8365
|
+
type: 'string',
|
|
8366
|
+
description: 'Bazel --output_base for read-only-cache CI environments'
|
|
8367
|
+
},
|
|
8368
|
+
bazelRc: {
|
|
8369
|
+
type: 'string',
|
|
8370
|
+
description: 'Path to additional .bazelrc fragments forwarded to bazel'
|
|
8371
|
+
},
|
|
8372
|
+
ecosystem: {
|
|
8373
|
+
type: 'string',
|
|
8374
|
+
isMultiple: true,
|
|
8375
|
+
description: 'Ecosystem(s) to extract; repeatable. Supported: maven, pypi. Default: maven.'
|
|
8376
|
+
},
|
|
8377
|
+
out: {
|
|
8378
|
+
type: 'string',
|
|
8379
|
+
description: 'Output directory for generated manifests; default: ./.socket/bazel-manifests/'
|
|
8380
|
+
},
|
|
8381
|
+
verbose: {
|
|
8382
|
+
type: 'boolean',
|
|
8383
|
+
description: 'Emit bounded Bazel diagnostics with argv, duration, exit status, and output sizes'
|
|
8384
|
+
}
|
|
8385
|
+
},
|
|
8386
|
+
help: (command, config) => `
|
|
8387
|
+
Usage
|
|
8388
|
+
$ ${command} [options] [CWD=.]
|
|
8389
|
+
|
|
8390
|
+
Options
|
|
8391
|
+
${utils.getFlagListOutput(config.flags)}
|
|
8392
|
+
|
|
8393
|
+
[beta] Generates Bazel SBOM manifests for Maven (\`maven_install.json\`)
|
|
8394
|
+
by running \`bazel query\` against discovered dependency repos.
|
|
8395
|
+
PyPI requirements generation is available with \`--ecosystem pypi\`.
|
|
8396
|
+
Output is consumed by
|
|
8397
|
+
\`socket scan create\`'s server-side parser.
|
|
8398
|
+
|
|
8399
|
+
--ecosystem may be repeated to select which ecosystems to extract.
|
|
8400
|
+
When omitted, Maven is generated by default. PyPI is explicit opt-in.
|
|
8401
|
+
|
|
8402
|
+
Note: this command generates dependency manifests for Bazel workspaces.
|
|
8403
|
+
It does not run reachability analysis.
|
|
8404
|
+
|
|
8405
|
+
To generate AND upload in one step, use \`socket scan create --auto-manifest\`
|
|
8406
|
+
instead — it detects Bazel workspaces, generates Maven manifests by
|
|
8407
|
+
default, and uploads the result. This subcommand is for generation only.
|
|
8408
|
+
|
|
8409
|
+
Examples
|
|
8410
|
+
$ ${command} .
|
|
8411
|
+
$ ${command} --ecosystem pypi .
|
|
8412
|
+
$ ${command} --ecosystem maven --ecosystem pypi .
|
|
8413
|
+
$ ${command} --bazel=/usr/local/bin/bazelisk .
|
|
8414
|
+
`
|
|
8415
|
+
};
|
|
8416
|
+
const cmdManifestBazel = {
|
|
8417
|
+
description: config$e.description,
|
|
8418
|
+
hidden: config$e.hidden,
|
|
8419
|
+
run: run$F
|
|
8420
|
+
};
|
|
8421
|
+
// Pure outcome-matrix evaluator. Exported so dispatcher behavior can be
|
|
8422
|
+
// unit-tested without spawning the CLI binary. Throws InputError on
|
|
8423
|
+
// failures that must propagate to a non-zero CLI exit; returns void on
|
|
8424
|
+
// success.
|
|
8425
|
+
//
|
|
8426
|
+
// - Hard failure: ok === false && !noEcosystemFound. The ecosystem was
|
|
8427
|
+
// detected (or the runner crashed), but extraction failed. Always a
|
|
8428
|
+
// non-zero exit, even when another ecosystem succeeded.
|
|
8429
|
+
// - No-discovery: noEcosystemFound === true. Genuinely absent ecosystem.
|
|
8430
|
+
// Auto-detect mode tolerates this when at least one other ecosystem
|
|
8431
|
+
// succeeded; explicit mode treats it as an error.
|
|
8432
|
+
function evaluateEcosystemOutcomes(outcomes, isExplicit) {
|
|
8433
|
+
const hardFailures = outcomes.filter(o => !o.ok && !o.noEcosystemFound);
|
|
8434
|
+
const noDiscoveries = outcomes.filter(o => o.noEcosystemFound);
|
|
8435
|
+
const successes = outcomes.filter(o => o.ok && o.manifestPath);
|
|
8436
|
+
if (!isExplicit) {
|
|
8437
|
+
if (hardFailures.length) {
|
|
8438
|
+
throw new utils.InputError(`Bazel auto-manifest generation hit hard failure(s) in ecosystem(s): ${hardFailures.map(f => f.ecosystem).join(', ')}.`);
|
|
8439
|
+
}
|
|
8440
|
+
if (successes.length) {
|
|
8441
|
+
return;
|
|
8442
|
+
}
|
|
8443
|
+
if (noDiscoveries.length === outcomes.length) {
|
|
8444
|
+
throw new utils.InputError('No supported Bazel ecosystems detected (maven, pypi). Ensure rules_jvm_external, rules_python pip_parse/pip_install/pip_repository, or pip.parse is configured.');
|
|
8445
|
+
}
|
|
8446
|
+
return;
|
|
8447
|
+
}
|
|
8448
|
+
|
|
8449
|
+
// Explicit mode: every requested ecosystem must succeed.
|
|
8450
|
+
if (noDiscoveries.length) {
|
|
8451
|
+
throw new utils.InputError(`No Bazel rules found for explicitly requested ecosystem(s): ${noDiscoveries.map(f => f.ecosystem).join(', ')}.`);
|
|
8452
|
+
}
|
|
8453
|
+
if (hardFailures.length) {
|
|
8454
|
+
throw new utils.InputError(`Bazel manifest generation failed for explicitly requested ecosystem(s): ${hardFailures.map(f => f.ecosystem).join(', ')}.`);
|
|
8455
|
+
}
|
|
8456
|
+
}
|
|
8457
|
+
async function run$F(argv, importMeta, {
|
|
8458
|
+
parentName
|
|
8459
|
+
}) {
|
|
8460
|
+
const cli = utils.meowOrExit({
|
|
8461
|
+
argv,
|
|
8462
|
+
config: config$e,
|
|
8463
|
+
importMeta,
|
|
8464
|
+
parentName
|
|
8465
|
+
});
|
|
8466
|
+
const {
|
|
8467
|
+
json = false,
|
|
8468
|
+
markdown = false
|
|
8469
|
+
} = cli.flags;
|
|
8470
|
+
const dryRun = !!cli.flags['dryRun'];
|
|
8471
|
+
|
|
8472
|
+
// TODO: Implement json/md further.
|
|
8473
|
+
const outputKind = utils.getOutputKind(json, markdown);
|
|
8474
|
+
let [cwd = '.'] = cli.input;
|
|
8475
|
+
// Note: path.resolve vs .join:
|
|
8476
|
+
// If given path is absolute then cwd should not affect it.
|
|
8477
|
+
cwd = path.resolve(process.cwd(), cwd);
|
|
8478
|
+
const sockJson = utils.readOrDefaultSocketJson(cwd);
|
|
8479
|
+
require$$9.debugFn('inspect', `override: ${constants.SOCKET_JSON} bazel`, sockJson?.defaults?.manifest?.bazel);
|
|
8480
|
+
const {
|
|
8481
|
+
ecosystem
|
|
8482
|
+
} = cli.flags;
|
|
8483
|
+
let {
|
|
8484
|
+
bazel,
|
|
8485
|
+
bazelFlags,
|
|
8486
|
+
bazelOutputBase,
|
|
8487
|
+
bazelRc,
|
|
8488
|
+
out,
|
|
8489
|
+
verbose
|
|
8490
|
+
} = cli.flags;
|
|
8491
|
+
|
|
8492
|
+
// Set defaults for any flag/arg that is not given. Check socket.json first.
|
|
8493
|
+
if (!bazel) {
|
|
8494
|
+
const defaultBazel = sockJson.defaults?.manifest?.bazel?.bazel ?? sockJson.defaults?.manifest?.bazel?.bin;
|
|
8495
|
+
if (defaultBazel) {
|
|
8496
|
+
bazel = defaultBazel;
|
|
8497
|
+
logger.logger.info(`Using default --bazel from ${constants.SOCKET_JSON}:`, bazel);
|
|
8498
|
+
}
|
|
8499
|
+
// Otherwise leave undefined; resolveBazelBinary performs the PATH
|
|
8500
|
+
// lookup for bazelisk/bazel.
|
|
8501
|
+
}
|
|
8502
|
+
if (!bazelFlags) {
|
|
8503
|
+
if (sockJson.defaults?.manifest?.bazel?.bazelFlags) {
|
|
8504
|
+
bazelFlags = sockJson.defaults?.manifest?.bazel?.bazelFlags;
|
|
8505
|
+
logger.logger.info(`Using default --bazel-flags from ${constants.SOCKET_JSON}:`, bazelFlags);
|
|
8506
|
+
} else {
|
|
8507
|
+
bazelFlags = '';
|
|
8508
|
+
}
|
|
8509
|
+
}
|
|
8510
|
+
if (!bazelOutputBase) {
|
|
8511
|
+
if (sockJson.defaults?.manifest?.bazel?.bazelOutputBase) {
|
|
8512
|
+
bazelOutputBase = sockJson.defaults?.manifest?.bazel?.bazelOutputBase;
|
|
8513
|
+
logger.logger.info(`Using default --bazel-output-base from ${constants.SOCKET_JSON}:`, bazelOutputBase);
|
|
8514
|
+
}
|
|
8515
|
+
}
|
|
8516
|
+
if (!bazelRc) {
|
|
8517
|
+
if (sockJson.defaults?.manifest?.bazel?.bazelRc) {
|
|
8518
|
+
bazelRc = sockJson.defaults?.manifest?.bazel?.bazelRc;
|
|
8519
|
+
logger.logger.info(`Using default --bazel-rc from ${constants.SOCKET_JSON}:`, bazelRc);
|
|
8520
|
+
}
|
|
8521
|
+
}
|
|
8522
|
+
if (!out) {
|
|
8523
|
+
if (sockJson.defaults?.manifest?.bazel?.out) {
|
|
8524
|
+
out = sockJson.defaults?.manifest?.bazel?.out;
|
|
8525
|
+
logger.logger.info(`Using default --out from ${constants.SOCKET_JSON}:`, out);
|
|
8526
|
+
} else {
|
|
8527
|
+
out = path.join(cwd, '.socket', 'bazel-manifests');
|
|
8528
|
+
}
|
|
8529
|
+
}
|
|
8530
|
+
if (verbose === undefined) {
|
|
8531
|
+
if (sockJson.defaults?.manifest?.bazel?.verbose !== undefined) {
|
|
8532
|
+
verbose = sockJson.defaults?.manifest?.bazel?.verbose;
|
|
8533
|
+
logger.logger.info(`Using default --verbose from ${constants.SOCKET_JSON}:`, verbose);
|
|
8534
|
+
} else {
|
|
8535
|
+
verbose = false;
|
|
8536
|
+
}
|
|
8537
|
+
}
|
|
8538
|
+
if (verbose) {
|
|
8539
|
+
logger.logger.group('- ', parentName, config$e.commandName, ':');
|
|
8540
|
+
logger.logger.group('- flags:', cli.flags);
|
|
8541
|
+
logger.logger.groupEnd();
|
|
8542
|
+
logger.logger.log('- input:', cli.input);
|
|
8543
|
+
logger.logger.groupEnd();
|
|
7448
8544
|
}
|
|
7449
8545
|
const wasValidInput = utils.checkCommandInput(outputKind, {
|
|
7450
8546
|
nook: true,
|
|
@@ -7466,15 +8562,56 @@ async function run$F(argv, importMeta, {
|
|
|
7466
8562
|
logger.logger.log(constants.default.DRY_RUN_BAILING_NOW);
|
|
7467
8563
|
return;
|
|
7468
8564
|
}
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
8565
|
+
|
|
8566
|
+
// Ecosystem dispatch: Maven is the default. PyPI is explicit opt-in because
|
|
8567
|
+
// its no-lockfile recovery value is narrower than Maven's inline-decl path.
|
|
8568
|
+
const wasExplicitEcosystemSelection = Array.isArray(ecosystem) && ecosystem.length > 0;
|
|
8569
|
+
const ecosystems = wasExplicitEcosystemSelection ? ecosystem : ['maven'];
|
|
8570
|
+
for (const eco of ecosystems) {
|
|
8571
|
+
if (!['maven', 'pypi'].includes(eco)) {
|
|
8572
|
+
throw new utils.InputError(`Unsupported --ecosystem value: ${eco}. Supported values: maven, pypi.`);
|
|
8573
|
+
}
|
|
8574
|
+
}
|
|
8575
|
+
const outcomes = [];
|
|
8576
|
+
for (const eco of ecosystems) {
|
|
8577
|
+
if (eco === 'maven') {
|
|
8578
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8579
|
+
const mavenResult = await extractBazelToMaven({
|
|
8580
|
+
bazelFlags: bazelFlags,
|
|
8581
|
+
bazelOutputBase: bazelOutputBase,
|
|
8582
|
+
bazelRc: bazelRc,
|
|
8583
|
+
bin: bazel,
|
|
8584
|
+
cwd,
|
|
8585
|
+
out: out,
|
|
8586
|
+
verbose: Boolean(verbose)
|
|
8587
|
+
});
|
|
8588
|
+
outcomes.push({
|
|
8589
|
+
ecosystem: 'maven',
|
|
8590
|
+
ok: mavenResult.ok,
|
|
8591
|
+
noEcosystemFound: mavenResult.noEcosystemFound,
|
|
8592
|
+
manifestPath: mavenResult.manifestPath
|
|
8593
|
+
});
|
|
8594
|
+
} else if (eco === 'pypi') {
|
|
8595
|
+
// eslint-disable-next-line no-await-in-loop
|
|
8596
|
+
const pypiResult = await extractBazelToPypi({
|
|
8597
|
+
bazelFlags: bazelFlags,
|
|
8598
|
+
bazelOutputBase: bazelOutputBase,
|
|
8599
|
+
bazelRc: bazelRc,
|
|
8600
|
+
bin: bazel,
|
|
8601
|
+
cwd,
|
|
8602
|
+
out: out,
|
|
8603
|
+
verbose: Boolean(verbose),
|
|
8604
|
+
explicitEcosystem: wasExplicitEcosystemSelection
|
|
8605
|
+
});
|
|
8606
|
+
outcomes.push({
|
|
8607
|
+
ecosystem: 'pypi',
|
|
8608
|
+
ok: pypiResult.ok,
|
|
8609
|
+
noEcosystemFound: pypiResult.noEcosystemFound,
|
|
8610
|
+
manifestPath: pypiResult.manifestPath
|
|
8611
|
+
});
|
|
8612
|
+
}
|
|
8613
|
+
}
|
|
8614
|
+
evaluateEcosystemOutcomes(outcomes, wasExplicitEcosystemSelection);
|
|
7478
8615
|
}
|
|
7479
8616
|
|
|
7480
8617
|
const config$d = {
|
|
@@ -17482,5 +18619,5 @@ process.on('unhandledRejection', async (reason, promise) => {
|
|
|
17482
18619
|
// eslint-disable-next-line n/no-process-exit
|
|
17483
18620
|
process.exit(1);
|
|
17484
18621
|
});
|
|
17485
|
-
//# debugId=
|
|
18622
|
+
//# debugId=932c44e2-d146-411e-8818-31f6bf237a5b
|
|
17486
18623
|
//# sourceMappingURL=cli.js.map
|