Diffinery

codician/diffinerypublic

Open-source, agent-first GitHub and GitHub Actions alternative that runs on your own Cloudflare account: git hosting, pull requests, code review and drop-in GitHub Actions YAML on Workers, Durable Objects, R2 and Containers.

101 lines · 5.0 KBRaw History
1import { existsSync, realpathSync } from "node:fs";
2import { mkdir, rm, symlink } from "node:fs/promises";
3import { randomUUID } from "node:crypto";
4import path from "node:path";
5import { errMsg, failure } from "../job.js";
6import { maxSatisfying, satisfies, which } from "../util.js";
7import type { Builtin } from "./index.js";
8import { fetchJson, fetchToFile, packageCache, probe, readVersionFile, toolArch } from "./tools.js";
9
10interface PyManifestEntry {
11 version: string;
12 stable: boolean;
13 files: Array<{ filename: string; arch: string; platform: string; platform_version?: string; download_url: string }>;
14}
15
16const MANIFEST = "https://raw.githubusercontent.com/actions/python-versions/main/versions-manifest.json";
17
18export const setupPython: Builtin = async (c) => {
19 let spec = c.input("python-version");
20 const file = c.input("python-version-file");
21 if (!spec && file) spec = await readVersionFile(c, file, "python");
22 if (!spec && existsSync(path.join(c.job.workspace, ".python-version"))) spec = await readVersionFile(c, ".python-version", "python");
23 spec = spec.trim();
24 if (/^pypy|graalpy/i.test(spec)) throw failure(`'${spec}' is not supported by the Diffinery setup-python built-in.`);
25 const env = c.toolEnv();
26 const installedRaw = await probe(c, "python3", ["--version"], env);
27 const installed = installedRaw?.replace(/^Python\s+/, "") ?? null;
28
29 let binDir: string;
30 let version: string;
31 const want = spec.replace(/-dev$/, "").replace(/\.x$/, "");
32 if (installed && (!spec || satisfies(installed, want))) {
33 c.log(`Using pre-installed Python ${installed}`);
34 const py3 = which("python3", env.PATH)!;
35 binDir = path.dirname(realpathSync(py3));
36 version = installed;
37 // make sure `python`/`pip` resolve to python3
38 if (!which("python", env.PATH) || !which("pip", env.PATH)) {
39 const shim = path.join(c.job.tempDir, "_python_shim");
40 await mkdir(shim, { recursive: true });
41 for (const [n, t] of [["python", "python3"], ["pip", "pip3"]] as const) {
42 const target = which(t, env.PATH);
43 const link = path.join(shim, n);
44 if (target && !existsSync(link)) await symlink(target, link).catch(() => {});
45 }
46 c.addPath(shim);
47 }
48 } else {
49 let manifest: PyManifestEntry[];
50 try {
51 manifest = await fetchJson<PyManifestEntry[]>(process.env.DIFFINERY_PYTHON_MANIFEST ?? MANIFEST);
52 } catch (e) {
53 throw failure(`Unable to fetch the Python versions manifest: ${errMsg(e)}`);
54 }
55 const arch = toolArch("python");
56 const candidates = manifest.filter((m) => m.stable && m.files.some((f) => f.platform === "linux" && f.arch === arch));
57 const v = maxSatisfying(candidates.map((m) => m.version), want);
58 if (!v) throw failure(`The version '${spec}' with architecture '${arch}' was not found for this operating system.`);
59 const entry = candidates.find((m) => m.version === v)!;
60 const files = entry.files.filter((f) => f.platform === "linux" && f.arch === arch);
61 const f = files.find((x) => x.platform_version === "24.04") ?? files.find((x) => x.platform_version === "22.04") ?? files[0]!;
62 const installDir = path.join(c.job.toolCache, "Python", v, arch);
63 if (!existsSync(`${installDir}.complete`)) {
64 const work = path.join(c.job.tempDir, `python-${randomUUID()}`);
65 await mkdir(work, { recursive: true });
66 try {
67 c.log(`Downloading ${f.download_url}`);
68 const tarball = path.join(work, "python.tar.gz");
69 await fetchToFile(f.download_url, tarball);
70 await c.run("tar", ["-xzf", tarball, "-C", work], { silent: true });
71 c.log("Running python installer (setup.sh)");
72 await c.run("bash", ["./setup.sh"], { cwd: work, env: c.toolEnv({ RUNNER_TOOL_CACHE: c.job.toolCache, AGENT_TOOLSDIRECTORY: c.job.toolCache }) });
73 } finally {
74 await rm(work, { recursive: true, force: true });
75 }
76 }
77 binDir = path.join(installDir, "bin");
78 version = v;
79 c.addPath(installDir);
80 c.addPath(binDir);
81 c.exportVariable("pythonLocation", installDir);
82 c.exportVariable("Python_ROOT_DIR", installDir);
83 c.exportVariable("Python3_ROOT_DIR", installDir);
84 c.exportVariable("PKG_CONFIG_PATH", path.join(installDir, "lib", "pkgconfig"));
85 c.exportVariable("LD_LIBRARY_PATH", [path.join(installDir, "lib"), process.env.LD_LIBRARY_PATH].filter(Boolean).join(":"));
86 }
87 c.setOutput("python-version", version);
88 c.setOutput("python-path", path.join(binDir, existsSync(path.join(binDir, "python")) ? "python" : "python3"));
89
90 const cache = c.input("cache").toLowerCase();
91 if (cache) {
92 if (cache !== "pip") throw failure(`Caching for '${cache}' is not supported by the Diffinery setup-python built-in (only pip).`);
93 const dir = await probe(c, "python3", ["-m", "pip", "cache", "dir"], c.toolEnv());
94 await packageCache(c, {
95 prefix: `setup-python-Linux-${toolArch("python")}-python-${version}-pip`,
96 paths: dir ? [dir] : [],
97 lockPatterns: ["**/requirements*.txt", "**/pyproject.toml"],
98 what: "pip",
99 });
100 }
101};