cd1ea5267c
Add node24 binary directory to PATH in action entry point so spawned processes (pnpm, npm, etc.) resolve to the correct node version instead of the runner's default v20. Improve delegate task result logging with success/failure status and summaries. Use collapsible log groups for dependency install output instead of raw streaming. Co-authored-by: Cursor <cursoragent@cursor.com>
199 lines
5.6 KiB
TypeScript
199 lines
5.6 KiB
TypeScript
import { existsSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import { log } from "../utils/cli.ts";
|
|
import { spawn } from "../utils/subprocess.ts";
|
|
import type {
|
|
PrepDefinition,
|
|
PrepOptions,
|
|
PythonPackageManager,
|
|
PythonPrepResult,
|
|
} from "./types.ts";
|
|
|
|
interface PythonConfig {
|
|
file: string;
|
|
tool: PythonPackageManager;
|
|
installCmd: string[];
|
|
}
|
|
|
|
// python dependency file patterns in priority order
|
|
const PYTHON_CONFIGS: PythonConfig[] = [
|
|
{
|
|
file: "requirements.txt",
|
|
tool: "pip",
|
|
installCmd: ["pip", "install", "-r", "requirements.txt"],
|
|
},
|
|
{
|
|
file: "pyproject.toml",
|
|
tool: "pip",
|
|
installCmd: ["pip", "install", "."],
|
|
},
|
|
{
|
|
file: "Pipfile",
|
|
tool: "pipenv",
|
|
installCmd: ["pipenv", "install"],
|
|
},
|
|
{
|
|
file: "Pipfile.lock",
|
|
tool: "pipenv",
|
|
installCmd: ["pipenv", "sync"],
|
|
},
|
|
{
|
|
file: "poetry.lock",
|
|
tool: "poetry",
|
|
installCmd: ["poetry", "install", "--no-interaction"],
|
|
},
|
|
{
|
|
file: "setup.py",
|
|
tool: "pip",
|
|
installCmd: ["pip", "install", "-e", "."],
|
|
},
|
|
];
|
|
|
|
// tool install commands (via pip)
|
|
const TOOL_INSTALL_COMMANDS: Record<string, string[]> = {
|
|
pipenv: ["pip", "install", "pipenv"],
|
|
poetry: ["pip", "install", "poetry"],
|
|
};
|
|
|
|
async function isCommandAvailable(command: string): Promise<boolean> {
|
|
const result = await spawn({
|
|
cmd: "which",
|
|
args: [command],
|
|
env: { PATH: process.env.PATH || "" },
|
|
});
|
|
return result.exitCode === 0;
|
|
}
|
|
|
|
async function installTool(name: string): Promise<string | null> {
|
|
const installCmd = TOOL_INSTALL_COMMANDS[name];
|
|
if (!installCmd) {
|
|
// tool doesn't need installation (e.g., pip)
|
|
return null;
|
|
}
|
|
|
|
log.info(`» installing ${name}...`);
|
|
const [cmd, ...args] = installCmd;
|
|
const result = await spawn({
|
|
cmd,
|
|
args,
|
|
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
|
onStderr: (chunk) => process.stderr.write(chunk),
|
|
});
|
|
|
|
if (result.exitCode !== 0) {
|
|
return result.stderr || `failed to install ${name}`;
|
|
}
|
|
|
|
log.info(`» installed ${name}`);
|
|
return null;
|
|
}
|
|
|
|
export const installPythonDependencies: PrepDefinition = {
|
|
name: "installPythonDependencies",
|
|
|
|
shouldRun: async () => {
|
|
// check if python is available
|
|
const hasPython = (await isCommandAvailable("python3")) || (await isCommandAvailable("python"));
|
|
if (!hasPython) {
|
|
return false;
|
|
}
|
|
|
|
// check if any python config file exists
|
|
const cwd = process.cwd();
|
|
return PYTHON_CONFIGS.some((config) => existsSync(join(cwd, config.file)));
|
|
},
|
|
|
|
run: async (options: PrepOptions): Promise<PythonPrepResult> => {
|
|
const cwd = process.cwd();
|
|
|
|
// find the first matching config
|
|
const config = PYTHON_CONFIGS.find((c) => existsSync(join(cwd, c.file)));
|
|
if (!config) {
|
|
return {
|
|
language: "python",
|
|
packageManager: "pip",
|
|
configFile: "unknown",
|
|
dependenciesInstalled: false,
|
|
issues: ["no python config file found"],
|
|
};
|
|
}
|
|
|
|
log.info(`» detected python config: ${config.file} (using ${config.tool})`);
|
|
|
|
// SECURITY: when shell is disabled, skip ALL python dependency installation.
|
|
// every python install path can potentially execute arbitrary code:
|
|
// - setup.py / pyproject.toml: directly execute build backends
|
|
// - requirements.txt: can contain "-e ." or local path references that
|
|
// trigger setup.py execution
|
|
// - Pipfile/poetry.lock: can contain path dependencies pointing to local
|
|
// directories with malicious setup.py
|
|
// - source distributions from PyPI also execute setup.py
|
|
// there is no equivalent of npm's --ignore-scripts for pip.
|
|
if (options.ignoreScripts) {
|
|
log.info(
|
|
`» skipping python install (shell disabled, python packages can execute arbitrary code)`
|
|
);
|
|
return {
|
|
language: "python",
|
|
packageManager: config.tool,
|
|
configFile: config.file,
|
|
dependenciesInstalled: false,
|
|
issues: [
|
|
`skipped: python dependency installation can execute arbitrary code (setup.py, build backends, local path references), which is blocked when shell is disabled`,
|
|
],
|
|
};
|
|
}
|
|
|
|
// check if the tool is available, install if needed
|
|
const isAvailable = await isCommandAvailable(config.tool);
|
|
if (!isAvailable) {
|
|
log.info(`» ${config.tool} not found, attempting to install...`);
|
|
const installError = await installTool(config.tool);
|
|
if (installError) {
|
|
return {
|
|
language: "python",
|
|
packageManager: config.tool,
|
|
configFile: config.file,
|
|
dependenciesInstalled: false,
|
|
issues: [installError],
|
|
};
|
|
}
|
|
}
|
|
|
|
// run the install command
|
|
const [cmd, ...args] = config.installCmd;
|
|
const fullCommand = `${cmd} ${args.join(" ")}`;
|
|
log.info(`» running: ${fullCommand}`);
|
|
const result = await spawn({
|
|
cmd,
|
|
args,
|
|
env: { PATH: process.env.PATH || "", HOME: process.env.HOME || "" },
|
|
});
|
|
|
|
const output = [result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
if (output) {
|
|
log.startGroup(`${fullCommand} output`);
|
|
log.info(output);
|
|
log.endGroup();
|
|
}
|
|
|
|
if (result.exitCode !== 0) {
|
|
return {
|
|
language: "python",
|
|
packageManager: config.tool,
|
|
configFile: config.file,
|
|
dependenciesInstalled: false,
|
|
issues: [output || `${cmd} exited with code ${result.exitCode}`],
|
|
};
|
|
}
|
|
|
|
return {
|
|
language: "python",
|
|
packageManager: config.tool,
|
|
configFile: config.file,
|
|
dependenciesInstalled: true,
|
|
issues: [],
|
|
};
|
|
},
|
|
};
|