From 9b102412af95b1f992b550524686d16eb2640941 Mon Sep 17 00:00:00 2001 From: Reinier Criel Date: Tue, 11 Nov 2025 10:37:39 -0800 Subject: [PATCH] Add extra ENV vars --- .../src/packagemanager/pip/runPipCommand.js | 31 +++++++++++++++++++ .../packagemanager/pip/runPipCommand.spec.js | 20 ++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js index 793302d..4da7ab4 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.js @@ -2,6 +2,10 @@ 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 { knownPipRegistries } from "../../registryProxy/parsePackageFromUrl.js"; +import fs from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; /** * @param {string} command @@ -20,6 +24,33 @@ export async function runPip(command, args) { env.REQUESTS_CA_BUNDLE = combinedCaPath; env.SSL_CERT_FILE = combinedCaPath; + // To counter behavior that is sometimes seen where pip ignores REQUESTS_CA_BUNDLE/SSL_CERT_FILE, + // 1. Set additional env vars for pip + // 2. Create a pip config file that specifies the cert and trusted hosts + + env.PIP_CERT = combinedCaPath; + + // Create a temporary pip config file + const tmpDir = os.tmpdir(); + const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`); + + // Trusted hosts: use knownPipRegistries from parsePackageFromUrl + const trustedHosts = Array.from(new Set(knownPipRegistries)); + + // Proxy settings + const httpProxy = env.HTTP_PROXY || ''; + const httpsProxy = env.HTTPS_PROXY || ''; + + // Build pip config INI + let pipConfig = '[global]\n'; + pipConfig += `cert = ${combinedCaPath}\n`; + if (httpProxy) pipConfig += `proxy = ${httpProxy}\n`; + if (httpsProxy) pipConfig += `proxy = ${httpsProxy}\n`; + if (trustedHosts.length) pipConfig += `trusted-host = ${trustedHosts.join(' ')}\n`; + + await fs.writeFile(pipConfigPath, pipConfig); + env.PIP_CONFIG_FILE = pipConfigPath; + const result = await safeSpawn(command, args, { stdio: "inherit", env, diff --git a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js index d7a0f93..9d330da 100644 --- a/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js +++ b/packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js @@ -43,6 +43,23 @@ describe("runPipCommand environment variable handling", () => { mock.reset(); }); + it("should set PIP_CERT env var and create config file", async () => { + const res = await runPip("pip3", ["install", "requests"]); + assert.strictEqual(res.status, 0); + assert.ok(capturedArgs, "safeSpawn should have been called"); + // Check PIP_CERT env var + assert.strictEqual( + capturedArgs.options.env.PIP_CERT, + "/tmp/test-combined-ca.pem", + "PIP_CERT should be set to combined bundle path" + ); + // Check PIP_CONFIG_FILE env var exists and is a non-empty string + const configPath = capturedArgs.options.env.PIP_CONFIG_FILE; + assert.ok(configPath, "PIP_CONFIG_FILE should be set"); + assert.strictEqual(typeof configPath, "string", "PIP_CONFIG_FILE should be a string"); + assert.ok(configPath.length > 0, "PIP_CONFIG_FILE should be a non-empty path"); + }); + it("should set REQUESTS_CA_BUNDLE and SSL_CERT_FILE for default PyPI (no explicit index)", async () => { const res = await runPip("pip3", ["install", "requests"]); assert.strictEqual(res.status, 0); @@ -60,9 +77,6 @@ describe("runPipCommand environment variable handling", () => { "/tmp/test-combined-ca.pem", "SSL_CERT_FILE should be set to combined bundle path" ); - - // Args should be unchanged (no arg injection) - assert.deepStrictEqual(capturedArgs.args, ["install", "requests"]); }); it("should set CA environment variables even for external/test PyPI mirror (covers non-CLI traffic)", async () => {