Merge pull request #12 from hexedtech/fix-ci-python

New Ci for Windows and no-abi build option for python
This commit is contained in:
əlemi 2024-09-21 13:27:43 +02:00 committed by GitHub
commit d7c4ef3891
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 152 additions and 84 deletions

View file

@ -89,6 +89,11 @@ jobs:
runs-on: ${{ matrix.platform.runner }}
strategy:
matrix:
versions:
- python: 3.8
features: py-noabi
- python: 3.x
features: py
platform:
- runner: windows-latest
target: x64
@ -101,7 +106,7 @@ jobs:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- uses: actions/setup-python@v5
with:
python-version: 3.x
python-version: ${{ matrix.versions.python }}
architecture: ${{ matrix.platform.target }}
- name: Build wheels
uses: PyO3/maturin-action@v1
@ -109,12 +114,12 @@ jobs:
working-directory: dist/py
target: ${{ matrix.platform.target }}
container: 'off'
args: --release --out ./build
args: --release --out ./build --features ${{ matrix.versions.features }}
sccache: 'true'
- name: Upload wheels
uses: actions/upload-artifact@v4
with:
name: codemp-py-windows-${{ matrix.platform.target }}
name: codemp-${{ matrix.versions.features }}-windows-${{ matrix.platform.target }}
path: dist/py/build
macos:

View file

@ -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"]

View file

@ -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();
}

View file

@ -1,8 +1,8 @@
[project]
name = "codemp"
version = "0.7.0"
version = "0.7.1"
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"

View file

@ -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.
///

View file

@ -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

View file

@ -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 {

View file

@ -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.

View file

@ -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<BufferControllerInner>);

View file

@ -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<ClientInner>);
#[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<str>) -> 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<str>) -> 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<str>, user_name: impl AsRef<str>) -> RemoteResult<()> {
self.0.session
pub async fn invite_to_workspace(
&self,
workspace_name: impl AsRef<str>,
user_name: impl AsRef<str>,
) -> 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<Vec<String>> {
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<str>) -> ConnectionResult<Workspace> {
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();

View file

@ -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<CursorControllerInner>);
@ -31,11 +37,23 @@ impl Controller<Cursor> 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<Option<Cursor>> {

View file

@ -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;

View file

@ -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<WorkspaceInner>);
@ -59,7 +59,8 @@ impl Workspace {
claims: tokio::sync::watch::Receiver<codemp_proto::common::Token>, // TODO ughh receiving this
) -> ConnectionResult<Self> {
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<String> {
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,