Refactor PyPI logic and cleanup

This commit is contained in:
Reinier Criel 2025-12-04 12:37:59 -08:00
parent e7cf3488b7
commit e211f531c5
11 changed files with 450 additions and 138 deletions

View file

@ -35,10 +35,11 @@ const state = {
/**
* @param {string} packageManagerName
* @param {{ tool: string, args: string[] }} [context] - Optional tool context for package managers like pip
*
* @return {PackageManager}
*/
export function initializePackageManager(packageManagerName) {
export function initializePackageManager(packageManagerName, context) {
if (packageManagerName === "npm") {
state.packageManagerName = createNpmPackageManager();
} else if (packageManagerName === "npx") {
@ -54,7 +55,7 @@ export function initializePackageManager(packageManagerName) {
} else if (packageManagerName === "bunx") {
state.packageManagerName = createBunxPackageManager();
} else if (packageManagerName === "pip") {
state.packageManagerName = createPipPackageManager();
state.packageManagerName = createPipPackageManager(context);
} else if (packageManagerName === "uv") {
state.packageManagerName = createUvPackageManager();
} else {

View file

@ -1,17 +1,21 @@
import { runPip } from "./runPipCommand.js";
import { getCurrentPipInvocation } from "./pipSettings.js";
import { PIP_COMMAND } from "./pipSettings.js";
/**
* @param {{ tool: string, args: string[] }} [context] - Optional context with tool name and args
* @returns {import("../currentPackageManager.js").PackageManager}
*/
export function createPipPackageManager() {
export function createPipPackageManager(context) {
const tool = context?.tool || PIP_COMMAND;
return {
/**
* @param {string[]} args
*/
runCommand: (args) => {
const invocation = getCurrentPipInvocation();
const fullArgs = [...invocation.args, ...args];
return runPip(invocation.command, fullArgs);
// Args from main.js are already stripped of --safe-chain-* flags
// We just pass the tool (e.g. "python3") and the args (e.g. ["-m", "pip", "install", ...])
return runPip(tool, args);
},
// For pip, rely solely on MITM proxy to detect/deny downloads from known registries.
isSupportedCommand: () => false,

View file

@ -1,30 +1,6 @@
export const PIP_PACKAGE_MANAGER = "pip";
// All supported python/pip invocations for Safe Chain interception
export const PIP_INVOCATIONS = {
PIP: { command: "pip", args: [] },
PIP3: { command: "pip3", args: [] },
PY_PIP: { command: "python", args: ["-m", "pip"] },
PY3_PIP: { command: "python3", args: ["-m", "pip"] },
PY_PIP3: { command: "python", args: ["-m", "pip3"] },
PY3_PIP3: { command: "python3", args: ["-m", "pip3"] }
};
/**
* @type {{ command: string, args: string[] }}
*/
let currentInvocation = PIP_INVOCATIONS.PY3_PIP; // Default to python3 -m pip
/**
* @param {{ command: string, args: string[] }} invocation
*/
export function setCurrentPipInvocation(invocation) {
currentInvocation = invocation;
}
/**
* @returns {{ command: string, args: string[] }}
*/
export function getCurrentPipInvocation() {
return currentInvocation;
}
export const PIP_COMMAND = "pip";
export const PIP3_COMMAND = "pip3";
export const PYTHON_COMMAND = "python";
export const PYTHON3_COMMAND = "python3";

View file

@ -2,12 +2,31 @@ import { ui } from "../../environment/userInteraction.js";
import { safeSpawn } from "../../utils/safeSpawn.js";
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
import { PIP_COMMAND, PIP3_COMMAND, PYTHON_COMMAND, PYTHON3_COMMAND } from "./pipSettings.js";
import fs from "node:fs/promises";
import fsSync from "node:fs";
import os from "node:os";
import path from "node:path";
import ini from "ini";
/**
* Checks if this pip invocation should bypass safe-chain and spawn directly.
* Returns true if the tool is python/python3 but NOT being run with -m pip/pip3.
* @param {string} command - The command executable
* @param {string[]} args - The arguments
* @returns {boolean}
*/
function shouldBypassSafeChain(command, args) {
if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) {
// Check if args start with -m pip
if (args.length >= 2 && args[0] === "-m" && (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)) {
return false;
}
return true;
}
return false;
}
/**
* Sets fallback CA bundle environment variables used by Python libraries.
* These are applied in addition to the PIP_CONFIG_FILE to ensure all Python
@ -49,11 +68,28 @@ function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) {
* Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow
* users to read/write persistent config. Only CA environment variables are set for these commands.
*
* @param {string} command - The pip command to execute (e.g., 'pip3')
* @param {string} command - The pip command executable (e.g., 'pip3' or 'python3')
* @param {string[]} args - Command line arguments to pass to pip
* @returns {Promise<{status: number}>} Exit status of the pip command
*/
export async function runPip(command, args) {
// Check if we should bypass safe-chain (python/python3 without -m pip)
if (shouldBypassSafeChain(command, args)) {
ui.writeVerbose(`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`);
// Spawn the ORIGINAL command with ORIGINAL args
const { spawn } = await import("child_process");
return new Promise((_resolve) => {
const proc = spawn(command, args, { stdio: "inherit" });
proc.on("exit", (/** @type {number | null} */ code) => {
process.exit(code ?? 0);
});
proc.on("error", (/** @type {Error} */ err) => {
ui.writeError(`Error executing command: ${err.message}`);
process.exit(1);
});
});
}
try {
const env = mergeSafeChainProxyEnvironmentVariables(process.env);