Repository navigation
Expand file tree
/
Copy pathpythonEnvironment.ts
More file actions
138 lines (128 loc) · 4.83 KB
/
Copy pathpythonEnvironment.ts
File metadata and controls
138 lines (128 loc) · 4.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/**
* Python environment activation for new terminals, the equivalent of VS
* Code's `python.terminal.activateEnvironment` with
* `python.defaultInterpreterPath`.
*
* The activation command is typed into the shell rather than injected as
* environment variables, so it runs after the user's rc files and survives
* ones that rebuild PATH.
*
* @module pythonEnvironment
*/
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Path from "effect/Path";
import { expandHomePath } from "@t3tools/provider-core/server/pathExpansion";
import * as HostProcess from "@t3tools/shared/HostProcess";
const VIRTUAL_ENVIRONMENT_DIRECTORIES = [".venv", "venv"] as const;
interface ShellSyntax {
/** Virtual environment activation scripts, in preference order. */
readonly scripts: ReadonlyArray<string>;
/** Runs a script in the current shell. */
readonly source: (scriptPath: string) => string;
readonly quote: (value: string) => string;
}
const posixQuote = (value: string) => `'${value.replaceAll("'", `'\\''`)}'`;
const powerShellQuote = (value: string) => `'${value.replaceAll("'", "''")}'`;
const cmdQuote = (value: string) => `"${value}"`;
function shellSyntax(shell: string): ShellSyntax | null {
const name = shell
.split(/[\\/]/)
.at(-1)!
.toLowerCase()
.replace(/\.exe$/, "");
const posix = (scripts: ReadonlyArray<string>, command: string): ShellSyntax => ({
scripts,
source: (script) => `${command} ${posixQuote(script)}`,
quote: posixQuote,
});
switch (name) {
case "bash":
case "zsh":
return posix(["activate"], "source");
case "sh":
case "dash":
case "ksh":
return posix(["activate"], ".");
case "fish":
return posix(["activate.fish"], "source");
case "csh":
case "tcsh":
return posix(["activate.csh"], "source");
case "pwsh":
case "powershell":
return {
// `python -m venv` writes Activate.ps1; uv writes activate.ps1.
scripts: ["Activate.ps1", "activate.ps1"],
source: (script) => `& ${powerShellQuote(script)}`,
quote: powerShellQuote,
};
case "cmd":
return { scripts: ["activate.bat"], source: cmdQuote, quote: cmdQuote };
default:
return null;
}
}
/**
* The line to type into a freshly started `shell` to activate a Python
* environment, or null when there is none or the shell is unsupported.
*
* `interpreterPath` names an interpreter or an environment directory; empty
* looks for a `.venv` or `venv` in `cwd`. Conda environments (those with a
* `conda-meta` directory) use `conda activate`, which needs the shell hook
* `conda init` installs. Anything else runs the environment's activate script.
*/
export const findPythonEnvironmentActivation = Effect.fn(
"terminal.findPythonEnvironmentActivation",
)(function* (input: {
readonly fileSystem: FileSystem.FileSystem;
readonly path: Path.Path;
readonly cwd: string;
readonly shell: string;
readonly platform: NodeJS.Platform;
readonly interpreterPath: string;
}) {
const syntax = shellSyntax(input.shell);
if (syntax === null) return null;
const { fileSystem, path } = input;
const exists = (target: string) =>
fileSystem.exists(target).pipe(Effect.orElseSucceed(() => false));
const binDirectory = input.platform === "win32" ? "Scripts" : "bin";
// The leading space keeps the line out of history in shells that ignore
// space-prefixed commands.
const line = (command: string) => ` ${command}\r`;
const findActivateScript = Effect.fn(function* (environment: string) {
for (const script of syntax.scripts) {
const scriptPath = path.join(environment, binDirectory, script);
if (yield* exists(scriptPath)) return scriptPath;
}
return null;
});
if (input.interpreterPath === "") {
for (const directory of VIRTUAL_ENVIRONMENT_DIRECTORIES) {
const script = yield* findActivateScript(path.join(input.cwd, directory));
if (script !== null) return line(syntax.source(script));
}
return null;
}
const configured = path.resolve(
input.cwd,
expandHomePath(input.interpreterPath, yield* HostProcess.HomeDirectory),
);
const info = yield* fileSystem.stat(configured).pipe(Effect.option);
if (info._tag === "None") return null;
let environment = configured;
if (info.value.type !== "Directory") {
// `<env>/bin/python` or `<env>\Scripts\python.exe`; conda on Windows
// keeps python.exe at the environment root.
const parent = path.dirname(configured);
environment = ["bin", "Scripts"].includes(path.basename(parent))
? path.dirname(parent)
: parent;
}
if (yield* exists(path.join(environment, "conda-meta"))) {
return line(`conda activate ${syntax.quote(environment)}`);
}
const script = yield* findActivateScript(environment);
return script === null ? null : line(syntax.source(script));
});