diff --git a/Cargo.toml b/Cargo.toml index 2698072..11d9853 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,7 @@ napi = { version = "2.16", features = ["full"], optional = true } napi-derive = { version="2.16", optional = true} # glue (python) -pyo3 = { version = "0.22", features = ["extension-module", "abi3-py38"], optional = true} +pyo3 = { version = "0.22", features = ["extension-module"], optional = true} # extra async-trait = { version = "0.1", optional = true } @@ -71,7 +71,8 @@ serialize = ["dep:serde", "uuid/serde"] rust = [] # used for ci matrix java = ["lazy_static", "jni", "tracing-subscriber"] js = ["napi-build", "tracing-subscriber", "napi", "napi-derive"] -py = ["pyo3", "tracing-subscriber", "pyo3-build-config"] +py-noabi = ["pyo3", "tracing-subscriber", "pyo3-build-config"] +py = ["py-noabi", "pyo3/abi3-py38"] lua = ["mlua-codemp-patch", "tracing-subscriber", "lazy_static", "serialize"] lua54 =["lua", "mlua-codemp-patch/lua54"] luajit = ["lua", "mlua-codemp-patch/luajit"] diff --git a/build.rs b/build.rs index fa74299..3dee00e 100644 --- a/build.rs +++ b/build.rs @@ -1,7 +1,7 @@ #[cfg(feature = "js")] extern crate napi_build; -#[cfg(feature = "py")] +#[cfg(any(feature = "py", feature = "py-noabi"))] extern crate pyo3_build_config; /// The main method of the buildscript, required by some glue modules. @@ -11,7 +11,7 @@ fn main() { napi_build::setup(); } - #[cfg(feature = "py")] + #[cfg(any(feature = "py", feature = "py-noabi"))] { pyo3_build_config::add_extension_module_link_args(); } diff --git a/dist/py/pyproject.toml b/dist/py/pyproject.toml index 3b881ec..7556caf 100644 --- a/dist/py/pyproject.toml +++ b/dist/py/pyproject.toml @@ -2,7 +2,7 @@ name = "codemp" version = "0.7.0" description = "code multiplexer" -requires-python = ">= 3.8" +requires-python = ">=3.8" license = "GPL-3.0-only" keywords = ["codemp", "cooperative", "rust", "python"] authors = [ @@ -28,7 +28,7 @@ requires = ["maturin>=1.0,<2.0"] build-backend = "maturin" [tool.maturin] -features = ["py", "pyo3/extension-module"] +features = ["py"] manifest-path = "../../Cargo.toml" python-source = "src" diff --git a/src/api/change.rs b/src/api/change.rs index 9c04aad..40e601e 100644 --- a/src/api/change.rs +++ b/src/api/change.rs @@ -2,7 +2,7 @@ //! A high-level representation of a change within a given buffer. /// An editor-friendly representation of a text change in a given buffer. -/// +/// /// It's expressed with a range of characters and a string of content that should replace them, /// allowing representation of any combination of deletions, insertions or replacements. /// @@ -22,7 +22,7 @@ /// #[derive(Clone, Debug, Default)] #[cfg_attr(feature = "js", napi_derive::napi(object))] -#[cfg_attr(feature = "py", pyo3::pyclass(get_all))] +#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyo3::pyclass(get_all))] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct TextChange { /// Range start of text change, as char indexes in buffer previous state. @@ -43,7 +43,7 @@ impl TextChange { } } -#[cfg_attr(feature = "py", pyo3::pymethods)] +#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyo3::pymethods)] impl TextChange { /// Returns true if this [`TextChange`] deletes existing text. /// diff --git a/src/api/config.rs b/src/api/config.rs index 8a70a10..58cfd2f 100644 --- a/src/api/config.rs +++ b/src/api/config.rs @@ -10,7 +10,10 @@ /// http{tls?'s':''}://{host}:{port} #[derive(Clone, Debug)] #[cfg_attr(feature = "js", napi_derive::napi(object))] -#[cfg_attr(feature = "py", pyo3::pyclass(get_all, set_all))] +#[cfg_attr( + any(feature = "py", feature = "py-noabi"), + pyo3::pyclass(get_all, set_all) +)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct Config { /// user identifier used to register, possibly your email diff --git a/src/api/cursor.rs b/src/api/cursor.rs index 1016ca5..7c1e4b5 100644 --- a/src/api/cursor.rs +++ b/src/api/cursor.rs @@ -1,12 +1,12 @@ //! ### Cursor //! Represents the position of a remote user's cursor. -#[cfg(feature = "py")] +#[cfg(any(feature = "py", feature = "py-noabi"))] use pyo3::prelude::*; /// User cursor position in a buffer #[derive(Clone, Debug, Default)] -#[cfg_attr(feature = "py", pyclass)] +#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyclass)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] // #[cfg_attr(feature = "py", pyo3(crate = "reexported::pyo3"))] pub struct Cursor { diff --git a/src/api/event.rs b/src/api/event.rs index 26e87df..c4c73c5 100644 --- a/src/api/event.rs +++ b/src/api/event.rs @@ -4,7 +4,7 @@ use codemp_proto::workspace::workspace_event::Event as WorkspaceEventInner; /// Event in a [crate::Workspace]. #[derive(Debug, Clone)] -#[cfg_attr(feature = "py", pyo3::pyclass)] +#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyo3::pyclass)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub enum Event { /// Fired when the file tree changes. diff --git a/src/buffer/controller.rs b/src/buffer/controller.rs index a690b05..f1fdbbc 100644 --- a/src/buffer/controller.rs +++ b/src/buffer/controller.rs @@ -18,7 +18,7 @@ use super::worker::DeltaRequest; /// Each buffer controller internally tracks the last acknowledged state, remaining always in sync /// with the server while allowing to procedurally receive changes while still sending new ones. #[derive(Debug, Clone)] -#[cfg_attr(feature = "py", pyo3::pyclass)] +#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyo3::pyclass)] #[cfg_attr(feature = "js", napi_derive::napi)] pub struct BufferController(pub(crate) Arc); diff --git a/src/client.rs b/src/client.rs index 4a586f8..2d4d03a 100644 --- a/src/client.rs +++ b/src/client.rs @@ -4,15 +4,25 @@ use std::sync::Arc; use dashmap::DashMap; -use tonic::{service::interceptor::InterceptedService, transport::{Channel, Endpoint}}; - -use crate::{api::User, errors::{ConnectionResult, RemoteResult}, ext::InternallyMutable, network, workspace::Workspace}; -use codemp_proto::{ - auth::{auth_client::AuthClient, LoginRequest}, - common::{Empty, Token}, session::{session_client::SessionClient, InviteRequest, WorkspaceRequest}, +use tonic::{ + service::interceptor::InterceptedService, + transport::{Channel, Endpoint}, }; -#[cfg(feature = "py")] +use crate::{ + api::User, + errors::{ConnectionResult, RemoteResult}, + ext::InternallyMutable, + network, + workspace::Workspace, +}; +use codemp_proto::{ + auth::{auth_client::AuthClient, LoginRequest}, + common::{Empty, Token}, + session::{session_client::SessionClient, InviteRequest, WorkspaceRequest}, +}; + +#[cfg(any(feature = "py", feature = "py-noabi"))] use pyo3::prelude::*; /// A `codemp` client handle. @@ -22,7 +32,7 @@ use pyo3::prelude::*; /// A new [`Client`] can be obtained with [`Client::connect`]. #[derive(Debug, Clone)] #[cfg_attr(feature = "js", napi_derive::napi)] -#[cfg_attr(feature = "py", pyclass)] +#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyclass)] pub struct Client(Arc); #[derive(Debug)] @@ -42,30 +52,37 @@ impl Client { let channel = Endpoint::from_shared(config.endpoint())?.connect().await?; let mut auth = AuthClient::new(channel.clone()); - let resp = auth.login(LoginRequest { - username: config.username.clone(), - password: config.password.clone(), - }) + let resp = auth + .login(LoginRequest { + username: config.username.clone(), + password: config.password.clone(), + }) .await? .into_inner(); let claims = InternallyMutable::new(resp.token); // TODO move this one into network.rs - let session = SessionClient::with_interceptor( - channel, network::SessionInterceptor(claims.channel()) - ); + let session = + SessionClient::with_interceptor(channel, network::SessionInterceptor(claims.channel())); Ok(Client(Arc::new(ClientInner { user: resp.user.into(), workspaces: DashMap::default(), - claims, auth, session, config + claims, + auth, + session, + config, }))) } /// Refresh session token. pub async fn refresh(&self) -> RemoteResult<()> { - let new_token = self.0.auth.clone().refresh(self.0.claims.get()) + let new_token = self + .0 + .auth + .clone() + .refresh(self.0.claims.get()) .await? .into_inner(); self.0.claims.set(new_token); @@ -74,25 +91,36 @@ impl Client { /// Attempt to create a new workspace with given name. pub async fn create_workspace(&self, name: impl AsRef) -> RemoteResult<()> { - self.0.session + self.0 + .session .clone() - .create_workspace(WorkspaceRequest { workspace: name.as_ref().to_string() }) + .create_workspace(WorkspaceRequest { + workspace: name.as_ref().to_string(), + }) .await?; Ok(()) } /// Delete an existing workspace if possible. pub async fn delete_workspace(&self, name: impl AsRef) -> RemoteResult<()> { - self.0.session + self.0 + .session .clone() - .delete_workspace(WorkspaceRequest { workspace: name.as_ref().to_string() }) + .delete_workspace(WorkspaceRequest { + workspace: name.as_ref().to_string(), + }) .await?; Ok(()) } /// Invite user with given username to the given workspace, if possible. - pub async fn invite_to_workspace(&self, workspace_name: impl AsRef, user_name: impl AsRef) -> RemoteResult<()> { - self.0.session + pub async fn invite_to_workspace( + &self, + workspace_name: impl AsRef, + user_name: impl AsRef, + ) -> RemoteResult<()> { + self.0 + .session .clone() .invite_to_workspace(InviteRequest { workspace: workspace_name.as_ref().to_string(), @@ -104,7 +132,9 @@ impl Client { /// List all available workspaces, also filtering between those owned and those invited to. pub async fn list_workspaces(&self, owned: bool, invited: bool) -> RemoteResult> { - let mut workspaces = self.0.session + let mut workspaces = self + .0 + .session .clone() .list_workspaces(Empty {}) .await? @@ -112,17 +142,25 @@ impl Client { let mut out = Vec::new(); - if owned { out.append(&mut workspaces.owned) } - if invited { out.append(&mut workspaces.invited) } + if owned { + out.append(&mut workspaces.owned) + } + if invited { + out.append(&mut workspaces.invited) + } Ok(out) } /// Join and return a [`Workspace`]. pub async fn join_workspace(&self, workspace: impl AsRef) -> ConnectionResult { - let token = self.0.session + let token = self + .0 + .session .clone() - .access_workspace(WorkspaceRequest { workspace: workspace.as_ref().to_string() }) + .access_workspace(WorkspaceRequest { + workspace: workspace.as_ref().to_string(), + }) .await? .into_inner(); diff --git a/src/cursor/controller.rs b/src/cursor/controller.rs index 014b1cf..8c2c8a4 100644 --- a/src/cursor/controller.rs +++ b/src/cursor/controller.rs @@ -5,14 +5,20 @@ use std::sync::Arc; use tokio::sync::{mpsc, oneshot, watch}; -use crate::{api::{controller::ControllerCallback, Controller, Cursor}, errors::ControllerResult}; -use codemp_proto::{cursor::{CursorPosition, RowCol}, files::BufferNode}; +use crate::{ + api::{controller::ControllerCallback, Controller, Cursor}, + errors::ControllerResult, +}; +use codemp_proto::{ + cursor::{CursorPosition, RowCol}, + files::BufferNode, +}; /// A [Controller] for asynchronously sending and receiving [Cursor] event. /// /// An unique [CursorController] exists for each active [crate::Workspace]. #[derive(Debug, Clone)] -#[cfg_attr(feature = "py", pyo3::pyclass)] +#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyo3::pyclass)] #[cfg_attr(feature = "js", napi_derive::napi)] pub struct CursorController(pub(crate) Arc); @@ -31,11 +37,23 @@ impl Controller for CursorController { if cursor.start > cursor.end { std::mem::swap(&mut cursor.start, &mut cursor.end); } - Ok(self.0.op.send(CursorPosition { - buffer: BufferNode { path: cursor.buffer }, - start: RowCol { row: cursor.start.0, col: cursor.start.1 }, - end: RowCol { row: cursor.end.0, col: cursor.end.1 }, - }).await?) + Ok(self + .0 + .op + .send(CursorPosition { + buffer: BufferNode { + path: cursor.buffer, + }, + start: RowCol { + row: cursor.start.0, + col: cursor.start.1, + }, + end: RowCol { + row: cursor.end.0, + col: cursor.end.1, + }, + }) + .await?) } async fn try_recv(&self) -> ControllerResult> { diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs index f9c4ae4..fa03449 100644 --- a/src/ffi/mod.rs +++ b/src/ffi/mod.rs @@ -1,20 +1,20 @@ //! # Foreign Function Interface //! `codemp` aims to be available as a library from as many programming languages as possible. //! To achieve this, we rely on Foreign Function Interface. -//! +//! //! ## JavaScript //! Our JavaScript glue is built with [`napi`](https://napi.rs). //! //! All async operations are handled on a separate tokio runtime, automatically managed by `napi`. //! Callbacks are safely scheduled to be called on the main loop thread. -//! +//! //! ## Python //! Our Python glue is built with [`PyO3`](https://pyo3.rs). //! //! All async operations return a `Promise`, which can we `.wait()`-ed to block and get the return //! value. The `Future` itself is run on a `tokio` runtime in a dedicated thread, which must be //! stared with `codemp.init()` before doing any async operations. -//! +//! //! ## Lua //! Our Lua glue is built with [`mlua`](https://github.com/mlua-rs/mlua). //! @@ -27,21 +27,21 @@ //! Note as Lua uses filename to locate entrypoint symbol, so shared object can't just have any name. //! Accepted filenames are `libcodemp.___`, `codemp.___`, `codemp_native.___`, `codemp_lua.___` (extension depends on your platform: `so` on linux, `dll` on windows, `dylib` on macos). //! Type hints are provided in `dist/lua/annotations.lua`, just include them in your language server: `---@module 'annotations'`. -//! +//! //! `codemp` is available as a rock on [LuaRocks](https://luarocks.org/modules/alemi/codemp), //! however LuaRocks compiles from source and will require having `cargo` installed. //! We provide pre-built binaries at [codemp.dev/releases/lua](https://codemp.dev/releases/lua/). //! **Please do not rely on this link, as our built binaries will likely move somewhere else soon!**. -//! +//! //! ## Java //! Our Java glue is built with [`jni`](https://github.com/jni-rs/jni-rs). //! //! Memory management is entirely delegated to the JVM's garbage collector. //! A more elegant solution than `Object.finalize()`, who is deprecated in newer Java versions, may be coming eventually. -//! +//! //! Exceptions coming from the native side have generally been made checked to imitate Rust's philosophy with `Result`. //! `JNIException`s are however unchecked: there is nothing you can do to recover from them, as they usually represent a severe error in the glue code. If they arise, it's probably a bug. -//! +//! /// java bindings, built with [jni] #[cfg(feature = "java")] @@ -56,5 +56,5 @@ pub mod lua; pub mod js; /// python bindings, built with [pyo3] -#[cfg(feature = "py")] +#[cfg(any(feature = "py", feature = "py-noabi"))] pub mod python; diff --git a/src/workspace.rs b/src/workspace.rs index b474121..a88f6b1 100644 --- a/src/workspace.rs +++ b/src/workspace.rs @@ -9,7 +9,7 @@ use crate::{ cursor::{self, worker::CursorWorker}, errors::{ConnectionResult, ControllerResult, RemoteResult}, ext::InternallyMutable, - network::Services + network::Services, }; use codemp_proto::{ @@ -33,7 +33,7 @@ use uuid::Uuid; use napi_derive::napi; #[derive(Debug, Clone)] -#[cfg_attr(feature = "py", pyo3::pyclass)] +#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyo3::pyclass)] #[cfg_attr(feature = "js", napi)] pub struct Workspace(Arc); @@ -59,7 +59,8 @@ impl Workspace { claims: tokio::sync::watch::Receiver, // TODO ughh receiving this ) -> ConnectionResult { let workspace_claim = InternallyMutable::new(token); - let services = Services::try_new(&config.endpoint(), claims, workspace_claim.channel()).await?; + let services = + Services::try_new(&config.endpoint(), claims, workspace_claim.channel()).await?; let ws_stream = services.ws().attach(Empty {}).await?.into_inner(); let (tx, rx) = mpsc::channel(128); @@ -126,12 +127,12 @@ impl Workspace { let (tx, rx) = mpsc::channel(256); let mut req = tonic::Request::new(tokio_stream::wrappers::ReceiverStream::new(rx)); - req.metadata_mut() - .insert( - "buffer", - tonic::metadata::MetadataValue::try_from(credentials.token) - .map_err(|e| tonic::Status::internal(format!("failed representing token to string: {e}")))?, - ); + req.metadata_mut().insert( + "buffer", + tonic::metadata::MetadataValue::try_from(credentials.token).map_err(|e| { + tonic::Status::internal(format!("failed representing token to string: {e}")) + })?, + ); let stream = self.0.services.buf().attach(req).await?.into_inner(); let worker = BufferWorker::new(self.0.user.id, path); @@ -282,14 +283,18 @@ impl Workspace { /// A filter may be applied, and it may be strict (equality check) or not (starts_with check). // #[cfg_attr(feature = "js", napi)] // https://github.com/napi-rs/napi-rs/issues/1120 pub fn filetree(&self, filter: Option<&str>, strict: bool) -> Vec { - self.0.filetree.iter() - .filter(|f| filter.map_or(true, |flt| { - if strict { - f.as_str() == flt - } else { - f.starts_with(flt) - } - })) + self.0 + .filetree + .iter() + .filter(|f| { + filter.map_or(true, |flt| { + if strict { + f.as_str() == flt + } else { + f.starts_with(flt) + } + }) + }) .map(|f| f.clone()) .collect() } @@ -315,9 +320,7 @@ impl Workspace { match ev { // user WorkspaceEventInner::Join(UserJoin { user }) => { - inner - .users - .insert(user.id.uuid(), user.into()); + inner.users.insert(user.id.uuid(), user.into()); } WorkspaceEventInner::Leave(UserLeave { user }) => { inner.users.remove(&user.id.uuid()); @@ -364,8 +367,8 @@ impl Drop for WorkspaceInner { } } -#[cfg_attr(feature = "py", pyo3::pyclass(eq, eq_int))] -#[cfg_attr(feature = "py", derive(PartialEq))] +#[cfg_attr(any(feature = "py", feature = "py-noabi"), pyo3::pyclass(eq, eq_int))] +#[cfg_attr(any(feature = "py", feature = "py-noabi"), derive(PartialEq))] pub enum DetachResult { NotAttached, Detaching,