diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml new file mode 100644 index 0000000..3317cf4 --- /dev/null +++ b/.github/workflows/python-build.yml @@ -0,0 +1,20 @@ +name: Python - Build + +on: [push, pull_request] + +env: + CARGO_TERM_COLOR: always + +jobs: + build-release-python: + name: "build-release-python" + runs-on: ubuntu-latest + defaults: + run: + working-directory: pyadb_client + steps: + - uses: actions/checkout@v4 + - name: Install Python dependencies + run: pip install . + - name: Build project + run: maturin build --release \ No newline at end of file diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml new file mode 100644 index 0000000..4224582 --- /dev/null +++ b/.github/workflows/python-release.yml @@ -0,0 +1,30 @@ +name: Rust - Release creation + +on: + release: + types: [created] + +jobs: + create-release: + runs-on: ubuntu-latest + defaults: + run: + working-directory: pyadb_client + + steps: + - uses: actions/checkout@v4 + + - name: Install Python dependencies + run: pip install . + + - name: Publish Python package + run: maturin publish --non-interactive + env: + MATURIN_PYPI_TOKEN: ${{ secrets.MATURIN_PYPI_TOKEN }} + + - name: "Publish GitHub artefacts" + uses: softprops/action-gh-release@v2 + with: + files: | + target/wheels/pyadb_client*.whl + target/wheels/pyadb_client*.tar.gz diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index a013908..a6e48f5 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -9,8 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - - name: "Checkout repository" - uses: actions/checkout@v4 + - uses: actions/checkout@v4 - name: "Set up Rust" uses: actions-rs/toolchain@v1 diff --git a/.gitignore b/.gitignore index c8e9e48..bf851e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ target -Cargo.lock -.vscode +/Cargo.lock +/.vscode +venv +/.mypy_cache \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 891f418..489ba06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["adb_cli", "adb_client"] +members = ["adb_cli", "adb_client", "pyadb_client"] resolver = "2" [workspace.package] diff --git a/README.md b/README.md index 51dce07..0173530 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,7 @@ Main features of this library: - Over **TCP/IP** - Implements hidden `adb` features, like `framebuffer` - Highly configurable +- Provides wrappers to use directly from Python code - Easy to use ! ## adb_client @@ -41,10 +42,16 @@ Improved documentation available [here](./adb_client/README.md). ## adb_cli Rust binary providing an improved version of Google's official `adb` CLI, by using `adb_client` library. -Provides an usage example of the library. +Provides a "real-world" usage example of this library. Improved documentation available [here](./adb_cli/README.md). +## pyadb_client + +Python wrapper using `adb_client` library to export classes usable directly from a Python environment. + +Improved documentation available [here](./pyadb_client/README.md) + ## Related publications - [Diving into ADB protocol internals (1/2)](https://www.synacktiv.com/publications/diving-into-adb-protocol-internals-12) diff --git a/pyadb_client/Cargo.toml b/pyadb_client/Cargo.toml new file mode 100644 index 0000000..1f5fbdf --- /dev/null +++ b/pyadb_client/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "pyadb_client" +description = "Python wrapper for adb_client library" +authors.workspace = true +edition.workspace = true +homepage.workspace = true +keywords.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true +readme = "README.md" + +[lib] +name = "pyadb_client" +crate-type = ["cdylib"] + +[dependencies] +anyhow = { version = "1.0.94" } +adb_client = { version = "2.0.6" } +pyo3 = { version = "0.23.3", features = ["extension-module", "anyhow"] } diff --git a/pyadb_client/README.md b/pyadb_client/README.md new file mode 100644 index 0000000..2a31265 --- /dev/null +++ b/pyadb_client/README.md @@ -0,0 +1,44 @@ +# pyadb_client + +Python library to communicate with ADB devices. Built on top of Rust `adb_client` library. + +## Examples + +### Use ADB server + +```python +server = pyadb_client.PyADBServer("127.0.0.1:5037") +for i, device in enumerate(server.devices()): + print(i, device.identifier, device.state) + +# Get only connected device +device = server.get_device() +print(device, device.identifier) +``` + +### Push a file on device + +```python +usb_device = PyADBUSBDevice.autodetect() +usb_device.push("file.txt", "/data/local/tmp/file.txt") +``` + +## Local development + +```bash +# Create Python virtual environment +cd pyadb_client +python3 -m venv .venv +source .venv/bin/activate + +# Install needed dependencies +pip install -e . + +# Build development package +maturin develop + +# Build release Python package +maturin build --release + +# Publish Python package +``` \ No newline at end of file diff --git a/pyadb_client/pyproject.toml b/pyadb_client/pyproject.toml new file mode 100644 index 0000000..be20b20 --- /dev/null +++ b/pyadb_client/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["maturin>=1,<2"] +build-backend = "maturin" + +[project] +dependencies = ["maturin", "patchelf"] +name = "pyadb_client" +dynamic = ["version"] # Let the build system automatically set package version +classifiers = [ + "Programming Language :: Rust", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: Implementation :: PyPy", +] diff --git a/pyadb_client/src/adb_server.rs b/pyadb_client/src/adb_server.rs new file mode 100644 index 0000000..54ffbaa --- /dev/null +++ b/pyadb_client/src/adb_server.rs @@ -0,0 +1,37 @@ +use std::net::SocketAddrV4; + +use adb_client::ADBServer; +use anyhow::Result; +use pyo3::{pyclass, pymethods, PyResult}; + +use crate::{PyADBServerDevice, PyDeviceShort}; + +#[pyclass] +pub struct PyADBServer(ADBServer); + +#[pymethods] +impl PyADBServer { + #[new] + pub fn new(address: String) -> PyResult { + let address = address.parse::()?; + Ok(ADBServer::new(address).into()) + } + + pub fn devices(&mut self) -> Result> { + Ok(self.0.devices()?.into_iter().map(|v| v.into()).collect()) + } + + pub fn get_device(&mut self) -> Result { + Ok(self.0.get_device()?.into()) + } + + pub fn get_device_by_name(&mut self, name: String) -> Result { + Ok(self.0.get_device_by_name(&name)?.into()) + } +} + +impl From for PyADBServer { + fn from(value: ADBServer) -> Self { + Self(value) + } +} diff --git a/pyadb_client/src/adb_server_device.rs b/pyadb_client/src/adb_server_device.rs new file mode 100644 index 0000000..6d18161 --- /dev/null +++ b/pyadb_client/src/adb_server_device.rs @@ -0,0 +1,38 @@ +use adb_client::{ADBDeviceExt, ADBServerDevice}; +use anyhow::Result; +use pyo3::{pyclass, pymethods}; +use std::{fs::File, path::PathBuf}; + +#[pyclass] +pub struct PyADBServerDevice(pub ADBServerDevice); + +#[pymethods] +impl PyADBServerDevice { + #[getter] + pub fn identifier(&self) -> String { + self.0.identifier.clone() + } + + pub fn shell_command(&mut self, commands: Vec) -> Result> { + let mut output = Vec::new(); + let commands: Vec<&str> = commands.iter().map(|x| &**x).collect(); + self.0.shell_command(&commands, &mut output)?; + Ok(output) + } + + pub fn push(&mut self, input: PathBuf, dest: PathBuf) -> Result<()> { + let mut reader = File::open(input)?; + Ok(self.0.push(&mut reader, dest.to_string_lossy())?) + } + + pub fn pull(&mut self, input: PathBuf, dest: PathBuf) -> Result<()> { + let mut writer = File::create(dest)?; + Ok(self.0.pull(&input.to_string_lossy(), &mut writer)?) + } +} + +impl From for PyADBServerDevice { + fn from(value: ADBServerDevice) -> Self { + Self(value) + } +} diff --git a/pyadb_client/src/adb_usb_device.rs b/pyadb_client/src/adb_usb_device.rs new file mode 100644 index 0000000..c6f95a4 --- /dev/null +++ b/pyadb_client/src/adb_usb_device.rs @@ -0,0 +1,39 @@ +use std::{fs::File, path::PathBuf}; + +use adb_client::{ADBDeviceExt, ADBUSBDevice}; +use anyhow::Result; +use pyo3::{pyclass, pymethods}; + +#[pyclass] +pub struct PyADBUSBDevice(ADBUSBDevice); + +#[pymethods] +impl PyADBUSBDevice { + #[staticmethod] + pub fn autodetect() -> Result { + Ok(ADBUSBDevice::autodetect()?.into()) + } + + pub fn shell_command(&mut self, commands: Vec) -> Result> { + let mut output = Vec::new(); + let commands: Vec<&str> = commands.iter().map(|x| &**x).collect(); + self.0.shell_command(&commands, &mut output)?; + Ok(output) + } + + pub fn push(&mut self, input: PathBuf, dest: PathBuf) -> Result<()> { + let mut reader = File::open(input)?; + Ok(self.0.push(&mut reader, &dest.to_string_lossy())?) + } + + pub fn pull(&mut self, input: PathBuf, dest: PathBuf) -> Result<()> { + let mut writer = File::create(dest)?; + Ok(self.0.pull(&input.to_string_lossy(), &mut writer)?) + } +} + +impl From for PyADBUSBDevice { + fn from(value: ADBUSBDevice) -> Self { + Self(value) + } +} diff --git a/pyadb_client/src/lib.rs b/pyadb_client/src/lib.rs new file mode 100644 index 0000000..f453b06 --- /dev/null +++ b/pyadb_client/src/lib.rs @@ -0,0 +1,20 @@ +mod adb_server; +mod adb_server_device; +mod adb_usb_device; +mod models; +pub use adb_server::*; +pub use adb_server_device::*; +pub use adb_usb_device::*; +pub use models::*; + +use pyo3::prelude::*; + +#[pymodule] +fn pyadb_client(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + + Ok(()) +} diff --git a/pyadb_client/src/models/devices.rs b/pyadb_client/src/models/devices.rs new file mode 100644 index 0000000..4a6ab21 --- /dev/null +++ b/pyadb_client/src/models/devices.rs @@ -0,0 +1,26 @@ +use adb_client::DeviceShort; +use pyo3::{pyclass, pymethods}; + +// Check https://docs.rs/rigetti-pyo3/latest/rigetti_pyo3 to automatically build this code + +#[pyclass] +pub struct PyDeviceShort(DeviceShort); + +#[pymethods] +impl PyDeviceShort { + #[getter] + pub fn identifier(&self) -> String { + self.0.identifier.clone() + } + + #[getter] + pub fn state(&self) -> String { + self.0.state.to_string() + } +} + +impl From for PyDeviceShort { + fn from(value: DeviceShort) -> Self { + Self(value) + } +} diff --git a/pyadb_client/src/models/mod.rs b/pyadb_client/src/models/mod.rs new file mode 100644 index 0000000..5912513 --- /dev/null +++ b/pyadb_client/src/models/mod.rs @@ -0,0 +1,2 @@ +mod devices; +pub use devices::PyDeviceShort;