300 lines
9.0 KiB
TypeScript
300 lines
9.0 KiB
TypeScript
#!/usr/bin/env runts -- --allow-env --allow-run
|
|
|
|
import {
|
|
execCommand,
|
|
execCommandShell,
|
|
existsPath,
|
|
joinPath,
|
|
log,
|
|
readFileToString,
|
|
resolveFilename,
|
|
} from "https://global.hatter.ink/script/get/@16/deno-commons-mod.ts";
|
|
import { parseArgs } from "jsr:@std/cli/parse-args";
|
|
import { writeStringToFile } from "../libraries/deno-commons-mod.ts";
|
|
|
|
const PYTHON_CONFIG_FILE = "~/.config/python-config.json";
|
|
const PYTHON_VENV_DEFAULT_BASE_DIR = "~/.venv/";
|
|
|
|
interface PythonConfig {
|
|
default_version?: string;
|
|
default_profile?: string;
|
|
python_venv_base_path?: string;
|
|
versions: Map<string, PythonVersion>;
|
|
profiles?: Map<string, PythonVenv>;
|
|
}
|
|
|
|
interface PythonVersion {
|
|
version: string;
|
|
path: string; // Python path dir or Python binary file
|
|
comment?: string;
|
|
}
|
|
|
|
interface PythonVenv {
|
|
version: string;
|
|
path: string;
|
|
comment?: string;
|
|
}
|
|
|
|
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),
|
|
);
|
|
}
|
|
|
|
async function findVirtualEnv(pythonVenvVersion: string): Promise<PythonVenv> {
|
|
const pythonConfig = await loadPythonConfig();
|
|
if (!pythonConfig.profiles) {
|
|
throw "No Python venvs configured";
|
|
}
|
|
const pythonVenv = pythonConfig.profiles[pythonVenvVersion];
|
|
if (!pythonVenv) {
|
|
throw `Python venv not found: ${pythonVenvVersion}`;
|
|
}
|
|
return pythonVenv;
|
|
}
|
|
|
|
async function newVirtualEnv(pythonVersion: string | null, pythonVenv: string) {
|
|
const pythonConfig = await loadPythonConfig();
|
|
const selectedPythonVersion = pythonVersion ||
|
|
pythonConfig.default_version || null;
|
|
if (!selectedPythonVersion) {
|
|
throw `No Python version assigned.`;
|
|
}
|
|
if (!selectedPythonVersion) {
|
|
throw `No Python venv assigned.`;
|
|
}
|
|
const pythonVersionProfile = pythonConfig.versions[pythonVersion];
|
|
if (!pythonVersionProfile) {
|
|
throw `Python version: ${pythonVersion} not found`;
|
|
}
|
|
log.success(`Found Python version: ${pythonVersion}`);
|
|
const pythonVenvProfile = pythonConfig.profiles[pythonVenv];
|
|
if (pythonVenvProfile) {
|
|
throw `Python venv already exists: ${pythonVenv}`;
|
|
}
|
|
const pythonVenvBaseDir = resolveFilename(
|
|
pythonConfig.python_venv_base_path || PYTHON_VENV_DEFAULT_BASE_DIR,
|
|
);
|
|
if (!existsPath(pythonVenvBaseDir)) {
|
|
log.info(`Make python venv base dir: ${pythonVenvBaseDir}`);
|
|
Deno.mkdirSync(pythonVenvBaseDir);
|
|
}
|
|
const pythonVenvDir = joinPath(pythonVenvBaseDir, pythonVenv);
|
|
if (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}, args: ${pythonVenvArgs}`,
|
|
);
|
|
await execCommandShell(python3Cmd, pythonVenvArgs);
|
|
}
|
|
|
|
async function isFile(path: string): Promise<boolean> {
|
|
const fileInfo = await Deno.stat(path);
|
|
return fileInfo?.isFile;
|
|
}
|
|
|
|
function handleHelp(_args: string[]) {
|
|
const help = [];
|
|
help.push(
|
|
`python.ts - Python version and virtual environment management tool
|
|
|
|
python.ts python - management python version [alias: py]
|
|
python.ts add-python - add python version [alias: py]
|
|
python.ts venv - management python virtual environment`,
|
|
);
|
|
// source <(cat ~/.venv-python-3.13.5/bin/activate)
|
|
// source <(python.ts venv test1)
|
|
console.log(help.join("\n"));
|
|
}
|
|
|
|
async function handlePython(args: string[]) {
|
|
const flags = parseArgs(Deno.args, {
|
|
boolean: ["help"],
|
|
string: ["version"],
|
|
alias: {
|
|
V: "version",
|
|
},
|
|
});
|
|
if (flags.help) {
|
|
console.log("Help massage for python");
|
|
return;
|
|
}
|
|
if (!flags.version) {
|
|
const pythonConfig = await loadPythonConfig();
|
|
if (!pythonConfig.versions) {
|
|
log.error("No Python versions configured");
|
|
return;
|
|
}
|
|
|
|
const versions = [];
|
|
let maxVersionLength = 0;
|
|
for (let version in pythonConfig.versions) {
|
|
versions.push(version);
|
|
if (version.length > maxVersionLength) {
|
|
maxVersionLength = version.length;
|
|
}
|
|
}
|
|
versions.sort();
|
|
|
|
console.log(`Found ${versions.length} Python version(s)`);
|
|
for (let version in pythonConfig.versions) {
|
|
const pythonVersion = pythonConfig.versions[version];
|
|
const versionPadding = " ".repeat(
|
|
maxVersionLength - version.length,
|
|
);
|
|
console.log(
|
|
`- Python ${version} ${versionPadding}: ${pythonVersion.path} [version: ${
|
|
pythonVersion.version || "unknown"
|
|
}]`,
|
|
);
|
|
}
|
|
|
|
return;
|
|
}
|
|
const pythonVirtualEnv = await findVirtualEnv(flags.version);
|
|
|
|
// TODO
|
|
}
|
|
|
|
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 (flags.help) {
|
|
console.log("Help massage for python add-python");
|
|
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(Deno.args, {
|
|
boolean: ["help"],
|
|
string: ["version"],
|
|
alias: {
|
|
V: "version",
|
|
},
|
|
});
|
|
if (flags.help) {
|
|
console.log("Help massage for venv");
|
|
return;
|
|
}
|
|
if (!flags.version) {
|
|
log.warn("Version missing");
|
|
return;
|
|
}
|
|
const pythonVirtualEnv = await findVirtualEnv(flags.version);
|
|
|
|
// TODO
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(Deno.args);
|
|
const [subcommand, ...remainingArgs] = args._;
|
|
|
|
if (!subcommand) {
|
|
log.warn("Subcommand not found, `python.ts help` for help message");
|
|
return;
|
|
}
|
|
|
|
switch (subcommand) {
|
|
case "help":
|
|
handleHelp(remainingArgs);
|
|
break;
|
|
case "py":
|
|
case "python":
|
|
await handlePython(remainingArgs);
|
|
break;
|
|
case "add-py":
|
|
case "add-python":
|
|
await handleAddPython(remainingArgs);
|
|
break;
|
|
case "venv":
|
|
await handleVenv(remainingArgs);
|
|
break;
|
|
default:
|
|
log.error(`Unknown subcommand: ${subcommand}`);
|
|
break;
|
|
}
|
|
}
|
|
|
|
main().catch((e) => log.error(e));
|