diff --git a/Cargo.toml b/Cargo.toml index 74a9997..e75b1eb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ resolver = "2" [workspace.dependencies] djls = { path = "crates/djls" } +djls-python = { path = "crates/djls-python" } anyhow = "1.0.94" pyo3 = { version = "0.23.3", features = ["auto-initialize", "abi3-py39"] } diff --git a/crates/djls-python/Cargo.toml b/crates/djls-python/Cargo.toml new file mode 100644 index 0000000..c29136f --- /dev/null +++ b/crates/djls-python/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "djls-python" +version = "0.0.0" +edition = "2021" + +[dependencies] +pyo3 = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } + +thiserror = "2.0.4" diff --git a/crates/djls-python/src/environment.rs b/crates/djls-python/src/environment.rs new file mode 100644 index 0000000..47ec36b --- /dev/null +++ b/crates/djls-python/src/environment.rs @@ -0,0 +1,68 @@ +use crate::python::{Interpreter, PythonError}; +use pyo3::prelude::*; +use std::fmt; +use std::path::PathBuf; + +#[derive(Debug)] +pub struct PythonEnvironment { + root: PathBuf, + build: Interpreter, + runtime: Interpreter, +} + +impl PythonEnvironment { + fn new(root: PathBuf, build: Interpreter, runtime: Interpreter) -> Self { + Self { + root, + build, + runtime, + } + } + + pub fn root(&self) -> &PathBuf { + &self.root + } + + pub fn build(&self) -> &Interpreter { + &self.build + } + + pub fn runtime(&self) -> &Interpreter { + &self.runtime + } + + pub fn initialize() -> Result { + Python::with_gil(|py| { + let build = Interpreter::for_build(py)?; + let runtime = Interpreter::for_runtime(build.sys_executable())?; + let root = runtime.sys_prefix().clone(); + + Ok(Self::new(root, build, runtime)) + }) + } +} + +impl fmt::Display for PythonEnvironment { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "Python Environment")?; + writeln!(f, "Root: {}", self.root.display())?; + writeln!(f)?; + writeln!(f, "Build Interpreter")?; + writeln!(f, "{}", self.build)?; + writeln!(f)?; + writeln!(f, "Runtime Interpreter")?; + write!(f, "{}", self.runtime) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum EnvironmentError { + #[error("Python error: {0}")] + Python(#[from] PyErr), + + #[error("Runtime error: {0}")] + Runtime(#[from] PythonError), + + #[error("Environment initialization failed: {0}")] + Init(String), +} diff --git a/crates/djls-python/src/lib.rs b/crates/djls-python/src/lib.rs new file mode 100644 index 0000000..47fb78e --- /dev/null +++ b/crates/djls-python/src/lib.rs @@ -0,0 +1,4 @@ +mod environment; +mod python; + +pub use environment::PythonEnvironment; diff --git a/crates/djls-python/src/python.rs b/crates/djls-python/src/python.rs new file mode 100644 index 0000000..dc1a15d --- /dev/null +++ b/crates/djls-python/src/python.rs @@ -0,0 +1,263 @@ +use pyo3::prelude::*; +use serde::Deserialize; +use std::fmt; +use std::path::PathBuf; +use std::process::Command; + +#[derive(Debug, Deserialize)] +pub struct VersionInfo { + pub major: u8, + pub minor: u8, + pub patch: u8, + pub suffix: Option, +} + +impl VersionInfo { + fn new(major: u8, minor: u8, patch: u8, suffix: Option) -> Self { + Self { + major, + minor, + patch, + suffix, + } + } + + pub fn from_python(py: Python) -> PyResult { + let version_info = py.version_info(); + + Ok(Self::new( + version_info.major, + version_info.minor, + version_info.patch, + version_info.suffix.map(String::from), + )) + } + + pub fn from_executable(executable: &PathBuf) -> Result { + let output = Command::new(executable) + .args(["-c", "import sys; print(sys.version.split()[0])"]) + .output()?; + + let version_str = String::from_utf8(output.stdout)?.trim().to_string(); + let parts: Vec<&str> = version_str.split('.').collect(); + + let major: u8 = parts[0].parse()?; + let minor: u8 = parts[1].parse()?; + + let last_part = parts[2]; + let (patch_str, suffix) = if last_part.chars().any(|c| !c.is_ascii_digit()) { + let idx = last_part.find(|c: char| !c.is_ascii_digit()).unwrap(); + (&last_part[..idx], Some(last_part[idx..].to_string())) + } else { + (last_part, None) + }; + let patch: u8 = patch_str.parse()?; + + Ok(Self::new(major, minor, patch, suffix)) + } +} + +impl fmt::Display for VersionInfo { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?; + if let Some(suffix) = &self.suffix { + write!(f, "{}", suffix)?; + } + Ok(()) + } +} + +#[derive(Debug, Deserialize)] +pub struct SysconfigPaths { + data: PathBuf, + include: PathBuf, + platinclude: PathBuf, + platlib: PathBuf, + platstdlib: PathBuf, + purelib: PathBuf, + scripts: PathBuf, + stdlib: PathBuf, +} + +impl SysconfigPaths { + pub fn from_python(py: Python) -> PyResult { + let sysconfig = py.import("sysconfig")?; + let paths = sysconfig.call_method0("get_paths")?; + + Ok(Self { + data: PathBuf::from(paths.get_item("data").unwrap().extract::()?), + include: PathBuf::from(paths.get_item("include").unwrap().extract::()?), + platinclude: PathBuf::from(paths.get_item("platinclude").unwrap().extract::()?), + platlib: PathBuf::from(paths.get_item("platlib").unwrap().extract::()?), + platstdlib: PathBuf::from(paths.get_item("platstdlib").unwrap().extract::()?), + purelib: PathBuf::from(paths.get_item("purelib").unwrap().extract::()?), + scripts: PathBuf::from(paths.get_item("scripts").unwrap().extract::()?), + stdlib: PathBuf::from(paths.get_item("stdlib").unwrap().extract::()?), + }) + } + + pub fn from_executable(executable: &PathBuf) -> Result { + let output = Command::new(executable) + .args([ + "-c", + r#" +import json +import sysconfig +paths = sysconfig.get_paths() +print(json.dumps(paths)) +"#, + ]) + .output()?; + + let output_str = String::from_utf8(output.stdout)?; + Ok(serde_json::from_str(&output_str)?) + } +} + +impl fmt::Display for SysconfigPaths { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "data: {}", self.data.display())?; + writeln!(f, "include: {}", self.include.display())?; + writeln!(f, "platinclude: {}", self.platinclude.display())?; + writeln!(f, "platlib: {}", self.platlib.display())?; + writeln!(f, "platstdlib: {}", self.platstdlib.display())?; + writeln!(f, "purelib: {}", self.purelib.display())?; + writeln!(f, "scripts: {}", self.scripts.display())?; + write!(f, "stdlib: {}", self.stdlib.display()) + } +} + +#[derive(Debug)] +pub struct Interpreter { + version_info: VersionInfo, + sysconfig_paths: SysconfigPaths, + sys_prefix: PathBuf, + sys_base_prefix: PathBuf, + sys_executable: PathBuf, + sys_path: Vec, +} + +impl Interpreter { + fn new( + version_info: VersionInfo, + sysconfig_paths: SysconfigPaths, + sys_prefix: PathBuf, + sys_base_prefix: PathBuf, + sys_executable: PathBuf, + sys_path: Vec, + ) -> Self { + Self { + version_info, + sysconfig_paths, + sys_prefix, + sys_base_prefix, + sys_executable, + sys_path, + } + } + + pub fn version_info(&self) -> &VersionInfo { + &self.version_info + } + + pub fn sysconfig_paths(&self) -> &SysconfigPaths { + &self.sysconfig_paths + } + + pub fn sys_prefix(&self) -> &PathBuf { + &self.sys_prefix + } + + pub fn sys_base_prefix(&self) -> &PathBuf { + &self.sys_base_prefix + } + + pub fn sys_executable(&self) -> &PathBuf { + &self.sys_executable + } + + pub fn sys_path(&self) -> &Vec { + &self.sys_path + } + + pub fn for_build(py: Python) -> PyResult { + let sys = py.import("sys")?; + + Ok(Self::new( + VersionInfo::from_python(py)?, + SysconfigPaths::from_python(py)?, + PathBuf::from(sys.getattr("prefix")?.extract::()?), + PathBuf::from(sys.getattr("base_prefix")?.extract::()?), + PathBuf::from(sys.getattr("executable")?.extract::()?), + sys.getattr("path")? + .extract::>()? + .into_iter() + .map(PathBuf::from) + .collect(), + )) + } + + pub fn for_runtime(executable: &PathBuf) -> Result { + let output = Command::new(executable) + .args([ + "-c", + r#" +import sys, json +print(json.dumps({ + 'prefix': sys.prefix, + 'base_prefix': sys.base_prefix, + 'executable': sys.executable, + 'path': [p for p in sys.path if p], +})) +"#, + ]) + .output()?; + + let output_str = String::from_utf8(output.stdout)?; + let sys_info: serde_json::Value = serde_json::from_str(&output_str)?; + + Ok(Self::new( + VersionInfo::from_executable(executable)?, + SysconfigPaths::from_executable(executable)?, + PathBuf::from(sys_info["prefix"].as_str().unwrap()), + PathBuf::from(sys_info["base_prefix"].as_str().unwrap()), + PathBuf::from(sys_info["executable"].as_str().unwrap()), + sys_info["path"] + .as_array() + .unwrap() + .iter() + .map(|p| PathBuf::from(p.as_str().unwrap())) + .collect(), + )) + } +} + +impl fmt::Display for Interpreter { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + writeln!(f, "Version: {}", self.version_info)?; + writeln!(f, "Executable: {}", self.sys_executable.display())?; + writeln!(f, "Prefix: {}", self.sys_prefix.display())?; + writeln!(f, "Base Prefix: {}", self.sys_base_prefix.display())?; + writeln!(f, "Paths:")?; + for path in &self.sys_path { + writeln!(f, "{}", path.display())?; + } + writeln!(f, "Sysconfig Paths:")?; + write!(f, "{}", self.sysconfig_paths) + } +} + +#[derive(Debug, thiserror::Error)] +pub enum PythonError { + #[error("IO error: {0}")] + Io(#[from] std::io::Error), + + #[error("UTF-8 conversion error: {0}")] + Utf8(#[from] std::string::FromUtf8Error), + + #[error("JSON parsing error: {0}")] + Json(#[from] serde_json::Error), + + #[error("Integer parsing error: {0}")] + Parse(#[from] std::num::ParseIntError), +} diff --git a/crates/djls/Cargo.toml b/crates/djls/Cargo.toml index 1745da5..21156e9 100644 --- a/crates/djls/Cargo.toml +++ b/crates/djls/Cargo.toml @@ -4,6 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] +djls-python = { workspace = true } + anyhow = { workspace = true } pyo3 = { workspace = true } serde = { workspace = true } diff --git a/crates/djls/src/main.rs b/crates/djls/src/main.rs index 1cfc8d0..81be139 100644 --- a/crates/djls/src/main.rs +++ b/crates/djls/src/main.rs @@ -3,9 +3,12 @@ use tower_lsp::jsonrpc::Result as LspResult; use tower_lsp::lsp_types::*; use tower_lsp::{Client, LanguageServer, LspService, Server}; +use djls_python::PythonEnvironment; + #[derive(Debug)] struct Backend { client: Client, + python: PythonEnvironment, } #[tower_lsp::async_trait] @@ -30,6 +33,10 @@ impl LanguageServer for Backend { self.client .log_message(MessageType::INFO, "server initialized!") .await; + + self.client + .log_message(MessageType::INFO, format!("\n{}", self.python)) + .await; } async fn shutdown(&self) -> LspResult<()> { @@ -39,10 +46,12 @@ impl LanguageServer for Backend { #[tokio::main] async fn main() -> Result<()> { + let python = PythonEnvironment::initialize()?; + let stdin = tokio::io::stdin(); let stdout = tokio::io::stdout(); - let (service, socket) = LspService::build(|client| Backend { client }).finish(); + let (service, socket) = LspService::build(|client| Backend { client, python }).finish(); Server::new(stdin, stdout, socket).serve(service).await;