551 lines
16 KiB
TypeScript
Executable File
551 lines
16 KiB
TypeScript
Executable File
#!/usr/bin/env runts -- --allow-env --allow-run --allow-import --allow-read --allow-write
|
|
|
|
import {
|
|
execCommand,
|
|
execCommandShell,
|
|
existsPath,
|
|
joinPath,
|
|
log,
|
|
ProcessBar,
|
|
readFileToString,
|
|
resolveFilename,
|
|
term,
|
|
writeStringToFile,
|
|
} from "https://global.hatter.ink/script/get/@18/deno-commons-mod.ts";
|
|
import {parseArgs} from "jsr:@std/cli/parse-args";
|
|
import {assertEquals} from "jsr:@std/assert";
|
|
|
|
const PYTHON_CONFIG_FILE = "~/.config/python-config.json";
|
|
const PYTHON_VENV_DEFAULT_BASE_DIR = "~/.venv/";
|
|
|
|
interface PythonConfig {
|
|
python_venv_base_path?: string;
|
|
versions: Record<string, PythonVersion>;
|
|
profiles?: Record<string, PythonVenv>;
|
|
}
|
|
|
|
interface PythonVersion {
|
|
version: string;
|
|
path: string; // Python path dir or Python binary file
|
|
comment?: string;
|
|
}
|
|
|
|
interface PythonVenv {
|
|
version: string;
|
|
path: string;
|
|
alias?: string[];
|
|
comment?: string;
|
|
}
|
|
|
|
function matchVersion(version: string, versionFilter: string): boolean {
|
|
if (version === versionFilter) {
|
|
return true;
|
|
}
|
|
return version.startsWith(
|
|
versionFilter.endsWith(".") ? versionFilter : (versionFilter + "."),
|
|
);
|
|
}
|
|
|
|
function findPythonVersion(
|
|
pythonConfig: PythonConfig,
|
|
version: string,
|
|
): PythonVersion | null {
|
|
if (!pythonConfig.versions) {
|
|
return null;
|
|
}
|
|
let pythonVersion: PythonVersion | null = null;
|
|
for (let ver in pythonConfig.versions) {
|
|
if (matchVersion(ver, version)) {
|
|
if (pythonVersion !== null) {
|
|
throw `Too many versions matched, version filter: ${version}`;
|
|
}
|
|
pythonVersion = pythonConfig.versions[ver];
|
|
}
|
|
}
|
|
return pythonVersion;
|
|
}
|
|
|
|
async function isFile(path: string): Promise<boolean> {
|
|
const fileInfo = await Deno.stat(path);
|
|
return fileInfo?.isFile;
|
|
}
|
|
|
|
async function loadPythonConfig(): Promise<PythonConfig> {
|
|
const pythonConfigFile = resolveFilename(PYTHON_CONFIG_FILE);
|
|
const pythonConfigJson = await readFileToString(pythonConfigFile);
|
|
if (!pythonConfigJson) {
|
|
throw `Could not read python config file: ${pythonConfigFile}`;
|
|
}
|
|
return JSON.parse(pythonConfigJson) as PythonConfig;
|
|
}
|
|
|
|
async function savePythonConfig(pythonConfig: PythonConfig): Promise<void> {
|
|
const pythonConfigFile = resolveFilename(PYTHON_CONFIG_FILE);
|
|
await writeStringToFile(
|
|
pythonConfigFile,
|
|
JSON.stringify(pythonConfig, null, 2),
|
|
);
|
|
}
|
|
|
|
function handleHelp(_args: string[]) {
|
|
const help = [];
|
|
help.push(
|
|
`${
|
|
term.green("python.ts")
|
|
} - Python version and virtual environment management tool
|
|
|
|
${term.green("python.ts")} ${
|
|
term.bold("python")
|
|
} - management python version ${
|
|
term.yellow("[alias: py, ver, version]")
|
|
}
|
|
${term.green("python.ts")} ${term.bold("add-python")} - add python version ${
|
|
term.yellow("[alias: add-py]")
|
|
}
|
|
${term.green("python.ts")} ${
|
|
term.bold("venv")
|
|
} - management python virtual environment ${
|
|
term.yellow("[alias: env]")
|
|
}
|
|
${term.green("python.ts")} ${
|
|
term.bold("add-venv")
|
|
} - add python virtual environment ${term.yellow("[alias: add-env]")}
|
|
${term.green("python.ts")} ${
|
|
term.bold("remove-venv")
|
|
} - remove python virtual environment ${
|
|
term.yellow("[alias: rm-venv, rm-env]")
|
|
}
|
|
${term.green("python.ts")} ${
|
|
term.bold("active-venv")
|
|
} - active python virtual environment ${
|
|
term.yellow("[alias: active, active-env]")
|
|
}`,
|
|
);
|
|
// source <(cat ~/.venv-python-3.13.5/bin/activate)
|
|
// source <(python.ts venv test1)
|
|
console.log(help.join("\n"));
|
|
}
|
|
|
|
async function addVirtualEnv(
|
|
pythonVersion: string | null,
|
|
pythonVenv: string,
|
|
alias?: string,
|
|
comment?: string,
|
|
): Promise<void> {
|
|
const pythonConfig = await loadPythonConfig();
|
|
if (!pythonVersion) {
|
|
throw `No Python version assigned.`;
|
|
}
|
|
if (!pythonVenv) {
|
|
throw `No Python virtual environment assigned.`;
|
|
}
|
|
const pythonVersionProfile = findPythonVersion(pythonConfig, pythonVersion);
|
|
if (!pythonVersionProfile) {
|
|
throw `Python version: ${pythonVersion} not found`;
|
|
}
|
|
const realPythonVersion = pythonVersionProfile.version || pythonVersion;
|
|
log.success(
|
|
`Found Python version: ${realPythonVersion}`,
|
|
);
|
|
const pythonVenvProfile = pythonConfig.profiles &&
|
|
pythonConfig.profiles[pythonVenv];
|
|
if (pythonVenvProfile) {
|
|
throw `Python virtual environment already exists: ${pythonVenv}`;
|
|
}
|
|
const pythonVenvBaseDir = resolveFilename(
|
|
pythonConfig.python_venv_base_path || PYTHON_VENV_DEFAULT_BASE_DIR,
|
|
);
|
|
if (!await existsPath(pythonVenvBaseDir)) {
|
|
log.info(
|
|
`Make python virtual environment base dir: ${pythonVenvBaseDir}`,
|
|
);
|
|
Deno.mkdirSync(pythonVenvBaseDir);
|
|
}
|
|
const pythonVenvLeafDir = `${pythonVenv}_python_${realPythonVersion}`;
|
|
const pythonVenvDir = joinPath(pythonVenvBaseDir, pythonVenvLeafDir);
|
|
if (await existsPath(pythonVenvDir)) {
|
|
throw `Python venv: ${pythonVenvDir} already exists`;
|
|
}
|
|
|
|
// python3 -m venv myenv
|
|
const python3Cmd = pythonVersionProfile.path;
|
|
log.success(`Found Python: ${python3Cmd}`);
|
|
const pythonVenvArgs = ["-m", "venv", pythonVenvDir];
|
|
|
|
log.info("Create Python venv, python:", [python3Cmd, pythonVenvArgs]);
|
|
await new ProcessBar("Creating Python virtual environment").call(
|
|
async () => {
|
|
await execCommandShell(python3Cmd, pythonVenvArgs);
|
|
},
|
|
);
|
|
|
|
const newPythonVenvProfile: PythonVenv = {
|
|
version: realPythonVersion,
|
|
path: pythonVenvDir,
|
|
comment: `Python venv:${pythonVenv}, version: ${realPythonVersion}${
|
|
comment ? (", user comment: " + comment) : ""
|
|
}`,
|
|
};
|
|
if (alias) {
|
|
newPythonVenvProfile.alias = [alias];
|
|
}
|
|
if (!pythonConfig.profiles) {
|
|
pythonConfig.profiles = {};
|
|
}
|
|
pythonConfig.profiles[pythonVenv] = newPythonVenvProfile;
|
|
await savePythonConfig(pythonConfig);
|
|
}
|
|
|
|
function versionSort(a: string, b: string): number {
|
|
const versionAParts = a.split(".");
|
|
const versionBParts = b.split(".");
|
|
const minLen = Math.min(versionAParts.length, versionBParts.length);
|
|
for (let i: number = 0; i < minLen; i++) {
|
|
const ai = parseInt(versionAParts[i], 10);
|
|
const bi = parseInt(versionBParts[i], 10);
|
|
if (ai !== bi) {
|
|
return (ai < bi) ? -1 : 1;
|
|
}
|
|
}
|
|
if (versionAParts.length === versionBParts.length) {
|
|
return 0;
|
|
}
|
|
return (versionAParts.length < versionBParts.length) ? -1 : 1;
|
|
}
|
|
|
|
async function handlePython(args: string[]) {
|
|
const flags = parseArgs(Deno.args, {
|
|
boolean: ["help"],
|
|
});
|
|
if (flags.help) {
|
|
console.log("Help massage for python");
|
|
return;
|
|
}
|
|
const pythonConfig = await loadPythonConfig();
|
|
if (!pythonConfig.versions) {
|
|
log.error("No Python versions configured");
|
|
return;
|
|
}
|
|
|
|
const versions: string[] = [];
|
|
let maxVersionLength = 0;
|
|
for (const version in pythonConfig.versions) {
|
|
versions.push(version);
|
|
if (version.length > maxVersionLength) {
|
|
maxVersionLength = version.length;
|
|
}
|
|
}
|
|
versions.sort(versionSort);
|
|
|
|
console.log(`Found ${versions.length} Python version(s)`);
|
|
for (const version of versions) {
|
|
const pythonVersion = pythonConfig.versions[version];
|
|
const versionPadding = " ".repeat(
|
|
maxVersionLength - version.length,
|
|
);
|
|
console.log(
|
|
"-",
|
|
"version:",
|
|
term.green(term.bold(version)),
|
|
";path:",
|
|
term.blue(pythonVersion.path),
|
|
";comment:",
|
|
term.yellow(term.under(pythonVersion.comment)),
|
|
);
|
|
// console.log("- Python:", version, pythonVersion,);
|
|
}
|
|
}
|
|
|
|
async function getPythonVersion(pythonBinPath: string): Promise<string> {
|
|
const pythonVersion = await execCommand(pythonBinPath, ["--version"]);
|
|
const pythonVersionLower = pythonVersion.assertSuccess()
|
|
.getStdoutAsStringThenTrim().toLowerCase();
|
|
if (!pythonVersionLower.startsWith("python ")) {
|
|
throw new Error(`Invalid Python version: ${pythonVersionLower}`);
|
|
}
|
|
return pythonVersionLower.substring("python ".length).trim();
|
|
}
|
|
|
|
async function handleAddPython(args: string[]) {
|
|
const flags = parseArgs(Deno.args, {
|
|
boolean: ["help"],
|
|
string: ["path"],
|
|
});
|
|
if (args.length === 0 || flags.help) {
|
|
console.log(`Help massage for python add-python
|
|
|
|
python.ts add-python --path python-path`);
|
|
return;
|
|
}
|
|
if (!flags.path) {
|
|
throw new Error("Path is empty");
|
|
}
|
|
const path = flags.path as string;
|
|
let pythonPath = path;
|
|
if (!path.includes("/")) {
|
|
const whichPath = await execCommand("which", [path]);
|
|
pythonPath = whichPath.assertSuccess().getStdoutAsStringThenTrim();
|
|
}
|
|
log.info(`Python path: ${pythonPath}`);
|
|
const isPythonPathFile = await isFile(pythonPath);
|
|
let pythonBinPath = "";
|
|
if (!isPythonPathFile) {
|
|
const pythonPath1 = joinPath(pythonPath, "bin", "python3");
|
|
if (await isFile(pythonPath1)) {
|
|
pythonBinPath = pythonPath1;
|
|
} else {
|
|
const pythonPath2 = joinPath(pythonPath, "bin", "python");
|
|
if (await isFile(pythonPath2)) {
|
|
pythonBinPath = pythonPath2;
|
|
} else {
|
|
throw new Error(
|
|
`Python bin path not found, python path: ${pythonPath}`,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
pythonBinPath = pythonPath;
|
|
}
|
|
const pythonVersion = await getPythonVersion(pythonBinPath);
|
|
log.info(`Python version: ${pythonVersion}`);
|
|
|
|
const pythonConfig = await loadPythonConfig();
|
|
|
|
if (!pythonConfig.versions) {
|
|
pythonConfig.versions = {};
|
|
}
|
|
if (pythonConfig.versions[pythonVersion]) {
|
|
throw new Error(`Python version ${pythonVersion} exists`);
|
|
}
|
|
const pyVersion: PythonVersion = {
|
|
version: pythonVersion,
|
|
path: pythonBinPath,
|
|
comment: `Python v${pythonVersion}`,
|
|
};
|
|
log.info("Found Python version", pyVersion);
|
|
pythonConfig.versions[pythonVersion] = pyVersion;
|
|
|
|
log.info("Save to Python config");
|
|
await savePythonConfig(pythonConfig);
|
|
}
|
|
|
|
async function handleVenv(args: string[]) {
|
|
const flags = parseArgs(args, {
|
|
boolean: ["help"],
|
|
string: ["name"],
|
|
alias: {
|
|
n: "name",
|
|
},
|
|
});
|
|
if (flags.help) {
|
|
console.log(`Help massage for venv
|
|
|
|
python.ts venv [--name|-n filter-name]`);
|
|
return;
|
|
}
|
|
let pythonConfig = await loadPythonConfig();
|
|
|
|
if (!pythonConfig.profiles) {
|
|
console.warn("No Python virtual environments found");
|
|
return;
|
|
}
|
|
|
|
let totalProfilesCount = 0;
|
|
let filterProfilesCount = 0;
|
|
Object.entries(pythonConfig.profiles).forEach(
|
|
([venv, pythonVenvProfile]) => {
|
|
totalProfilesCount++;
|
|
if (flags.name && venv.indexOf(flags.name) < 0) {
|
|
return;
|
|
}
|
|
filterProfilesCount++;
|
|
console.log(
|
|
"-",
|
|
term.blue(term.bold(term.under(venv))),
|
|
term.yellow(
|
|
pythonVenvProfile.alias
|
|
? `[alias: ${pythonVenvProfile.alias.join(", ")}]`
|
|
: "-",
|
|
),
|
|
pythonVenvProfile,
|
|
`, active virtual environment command: ${
|
|
term.under("source <(python.ts active " + venv + ")")
|
|
}`,
|
|
);
|
|
},
|
|
);
|
|
|
|
if (totalProfilesCount !== filterProfilesCount) {
|
|
console.log(
|
|
`\nFilter profiles with '${flags.name}': ${filterProfilesCount} of ${totalProfilesCount}`,
|
|
);
|
|
}
|
|
console.log(
|
|
"\nNOTE: use --name|-n filter virtual environments, use `source <(python.rs active[-venv] venv-name)`",
|
|
);
|
|
}
|
|
|
|
async function handleAddVenv(args: string[]) {
|
|
const flags = parseArgs(args, {
|
|
boolean: ["help"],
|
|
string: ["version", "venv", "comment", "alias"],
|
|
alias: {
|
|
V: "version",
|
|
c: "comment",
|
|
},
|
|
});
|
|
if (args.length === 0 || flags.help) {
|
|
console.log(`Help massage for add-venv
|
|
|
|
python.ts add-venv --version 3.10 --venv test-env [--comment comment]`);
|
|
return;
|
|
}
|
|
if (!flags.version) {
|
|
log.error("Version missing");
|
|
return;
|
|
}
|
|
if (!flags.venv) {
|
|
log.error("Venv is missing");
|
|
return;
|
|
}
|
|
await addVirtualEnv(flags.version, flags.venv, flags.alias, flags.comment);
|
|
}
|
|
|
|
async function handleRemoveVenv(args: string[]) {
|
|
const flags = parseArgs(args, {
|
|
boolean: ["help"],
|
|
string: ["venv"],
|
|
});
|
|
if (args.length === 0 || flags.help) {
|
|
console.log(`Help massage for remove-venv
|
|
|
|
python.ts remove-venv --venv test-env`);
|
|
return;
|
|
}
|
|
if (!flags.venv) {
|
|
log.error("Venv is missing");
|
|
return;
|
|
}
|
|
const pythonConfig = await loadPythonConfig();
|
|
const pythonVenvProfile = pythonConfig.profiles &&
|
|
pythonConfig.profiles[flags.venv];
|
|
if (!pythonVenvProfile) {
|
|
throw `Python virtual environment not exists: ${flags.venv}`;
|
|
}
|
|
|
|
console.log(
|
|
`Pending remove virtual environment [PLEASE CONFIRM]: `,
|
|
pythonVenvProfile,
|
|
);
|
|
|
|
if (confirm("Please confirm:")) {
|
|
const path = pythonVenvProfile.path;
|
|
log.info(`Remove path: ${path}`);
|
|
await new ProcessBar(`Removing path ${path}`).call(async () => {
|
|
await Deno.remove(path, { recursive: true });
|
|
});
|
|
if (pythonConfig.profiles) {
|
|
delete pythonConfig.profiles[flags.venv];
|
|
}
|
|
await savePythonConfig(pythonConfig);
|
|
log.success(
|
|
`Virtual environment: '${flags.venv}' removed, path: ${path}`,
|
|
);
|
|
} else {
|
|
console.log(
|
|
`Your skip virtual environment removal`,
|
|
);
|
|
}
|
|
}
|
|
|
|
async function handleActiveVenv(args: string[]) {
|
|
if (args.length === 0) {
|
|
console.log(`Help massage for active-venv
|
|
|
|
source <(python.rs active[-venv] test-env)`);
|
|
return;
|
|
}
|
|
|
|
const venv = args[0];
|
|
const pythonConfig = await loadPythonConfig();
|
|
const pythonVenvProfile = pythonConfig.profiles &&
|
|
pythonConfig.profiles[venv];
|
|
if (!pythonVenvProfile) {
|
|
throw `Python virtual environment not exists: ${venv}`;
|
|
}
|
|
|
|
const pythonVenvActiveFile = joinPath(
|
|
pythonVenvProfile.path,
|
|
"bin",
|
|
"activate",
|
|
);
|
|
const pythonVenvActiveFileContent = await readFileToString(
|
|
pythonVenvActiveFile,
|
|
);
|
|
console.log(pythonVenvActiveFileContent);
|
|
}
|
|
|
|
async function main() {
|
|
const args = Deno.args;
|
|
|
|
if (args.length === 0) {
|
|
log.warn("No subcommand assigned");
|
|
handleHelp([]);
|
|
return;
|
|
}
|
|
|
|
const subcommand = args[0];
|
|
const remainingArgs = args.slice(1);
|
|
switch (subcommand) {
|
|
case "help":
|
|
handleHelp(remainingArgs);
|
|
break;
|
|
case "py":
|
|
case "ver":
|
|
case "version":
|
|
case "python":
|
|
await handlePython(remainingArgs);
|
|
break;
|
|
case "add-py":
|
|
case "add-python":
|
|
await handleAddPython(remainingArgs);
|
|
break;
|
|
case "env":
|
|
case "venv":
|
|
await handleVenv(remainingArgs);
|
|
break;
|
|
case "add-env":
|
|
case "add-venv":
|
|
await handleAddVenv(remainingArgs);
|
|
break;
|
|
case "rm-env":
|
|
case "rm-venv":
|
|
case "remote-env":
|
|
case "remove-venv":
|
|
await handleRemoveVenv(remainingArgs);
|
|
break;
|
|
case "active":
|
|
case "active-env":
|
|
case "active-venv":
|
|
await handleActiveVenv(remainingArgs);
|
|
break;
|
|
default:
|
|
log.error(`Unknown subcommand: ${subcommand}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
main().catch((e) => log.error(e));
|
|
|
|
Deno.test("versionSort", () => {
|
|
const versions1 = ["3.10", "3.7"];
|
|
versions1.sort(versionSort);
|
|
assertEquals(["3.7", "3.10"], versions1);
|
|
const versions2 = ["3.10", "3.10.1", "3.11", "3.7"];
|
|
versions2.sort(versionSort);
|
|
assertEquals(["3.7", "3.10", "3.10.1", "3.11"], versions2);
|
|
});
|
|
|
|
// @SCRIPT-SIGNATURE-V1: yk-r1.ES256.20260127T224153+08:00.MEUCIQCuTKNb0y+N137tvQw+
|
|
// 47AFPWoCz5WaUSCJOUpcQBKDNwIgFxvsWvHCgWh5jpCjoQWURwLjAHsN9ze5K5X7KkVUuEM=
|