| 
 | 1 | +import fs from "node:fs";  | 
 | 2 | +import { $ } from "execa";  | 
 | 3 | +import { assert } from "@std/assert";  | 
 | 4 | +import { BuildManifest } from "@trigger.dev/core/v3";  | 
 | 5 | +import { BuildContext, BuildExtension } from "@trigger.dev/core/v3/build";  | 
 | 6 | +import { logger } from "@trigger.dev/sdk/v3";  | 
 | 7 | + | 
 | 8 | +export type PythonOptions = {  | 
 | 9 | +  requirements?: string[];  | 
 | 10 | +  requirementsFile?: string;  | 
 | 11 | +  /**  | 
 | 12 | +   * [Dev-only] The path to the python binary.  | 
 | 13 | +   *  | 
 | 14 | +   * @remarks  | 
 | 15 | +   * This option is typically used during local development or in specific testing environments  | 
 | 16 | +   * where a particular Python installation needs to be targeted.  It should point to the full path of the python executable.  | 
 | 17 | +   *  | 
 | 18 | +   * Example: `/usr/bin/python3` or `C:\\Python39\\python.exe`  | 
 | 19 | +   */  | 
 | 20 | +  pythonBinaryPath?: string;  | 
 | 21 | +};  | 
 | 22 | + | 
 | 23 | +export function pythonExtension(options: PythonOptions = {}): BuildExtension {  | 
 | 24 | +  return new PythonExtension(options);  | 
 | 25 | +}  | 
 | 26 | + | 
 | 27 | +class PythonExtension implements BuildExtension {  | 
 | 28 | +  public readonly name = "PythonExtension";  | 
 | 29 | + | 
 | 30 | +  constructor(private options: PythonOptions = {}) {  | 
 | 31 | +    assert(  | 
 | 32 | +      !(this.options.requirements && this.options.requirementsFile),  | 
 | 33 | +      "Cannot specify both requirements and requirementsFile"  | 
 | 34 | +    );  | 
 | 35 | + | 
 | 36 | +    if (this.options.requirementsFile) {  | 
 | 37 | +      this.options.requirements = fs  | 
 | 38 | +        .readFileSync(this.options.requirementsFile, "utf-8")  | 
 | 39 | +        .split("\n");  | 
 | 40 | +    }  | 
 | 41 | +  }  | 
 | 42 | + | 
 | 43 | +  async onBuildComplete(context: BuildContext, manifest: BuildManifest) {  | 
 | 44 | +    if (context.target === "dev") {  | 
 | 45 | +      if (this.options.pythonBinaryPath) {  | 
 | 46 | +        process.env.PYTHON_BIN_PATH = this.options.pythonBinaryPath;  | 
 | 47 | +      }  | 
 | 48 | + | 
 | 49 | +      return;  | 
 | 50 | +    }  | 
 | 51 | + | 
 | 52 | +    context.logger.debug(`Adding ${this.name} to the build`);  | 
 | 53 | + | 
 | 54 | +    context.addLayer({  | 
 | 55 | +      id: "python-extension",  | 
 | 56 | +      build: {  | 
 | 57 | +        env: {  | 
 | 58 | +          REQUIREMENTS_CONTENT: this.options.requirements?.join("\n") || "",  | 
 | 59 | +        },  | 
 | 60 | +      },  | 
 | 61 | +      image: {  | 
 | 62 | +        instructions: `  | 
 | 63 | +          # Install Python  | 
 | 64 | +          RUN apt-get update && apt-get install -y --no-install-recommends \  | 
 | 65 | +              python3 python3-pip python3-venv && \  | 
 | 66 | +              apt-get clean && rm -rf /var/lib/apt/lists/*  | 
 | 67 | +
  | 
 | 68 | +          # Set up Python environment  | 
 | 69 | +          RUN python3 -m venv /opt/venv  | 
 | 70 | +          ENV PATH="/opt/venv/bin:$PATH"  | 
 | 71 | +
  | 
 | 72 | +          ARG REQUIREMENTS_CONTENT  | 
 | 73 | +          RUN echo "$REQUIREMENTS_CONTENT" > requirements.txt  | 
 | 74 | +
  | 
 | 75 | +          # Install dependenciess  | 
 | 76 | +          RUN pip install --no-cache-dir -r requirements.txt  | 
 | 77 | +        `.split("\n"),  | 
 | 78 | +      },  | 
 | 79 | +      deploy: {  | 
 | 80 | +        env: {  | 
 | 81 | +          PYTHON_BIN_PATH: `/opt/venv/bin/python`,  | 
 | 82 | +        },  | 
 | 83 | +        override: true,  | 
 | 84 | +      },  | 
 | 85 | +    });  | 
 | 86 | +  }  | 
 | 87 | +}  | 
 | 88 | + | 
 | 89 | +export const run = async (  | 
 | 90 | +  args?: string,  | 
 | 91 | +  options: Parameters<typeof $>[1] = {}  | 
 | 92 | +) => {  | 
 | 93 | +  const cmd = `${process.env.PYTHON_BIN_PATH || "python"} ${args}`;  | 
 | 94 | + | 
 | 95 | +  logger.debug(  | 
 | 96 | +    `Running python:\t${cmd} ${options.input ? `(with stdin)` : ""}`,  | 
 | 97 | +    options  | 
 | 98 | +  );  | 
 | 99 | + | 
 | 100 | +  const result = await $({  | 
 | 101 | +    shell: true,  | 
 | 102 | +    ...options,  | 
 | 103 | +  })`${cmd}`;  | 
 | 104 | + | 
 | 105 | +  try {  | 
 | 106 | +    assert(!result.failed, `Command failed: ${result.stderr}`);  | 
 | 107 | +    assert(result.exitCode === 0, `Non-zero exit code: ${result.exitCode}`);  | 
 | 108 | +  } catch (e) {  | 
 | 109 | +    logger.error(e.message, result);  | 
 | 110 | +    throw e;  | 
 | 111 | +  }  | 
 | 112 | + | 
 | 113 | +  return result;  | 
 | 114 | +};  | 
 | 115 | + | 
 | 116 | +export default run;  | 
0 commit comments