├── .gitignore ├── tests ├── cli-help.rs ├── snapshots │ └── cli-help-long.txt └── integration.rs ├── LICENSE-MIT ├── RELEASE.adoc ├── Cargo.toml ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── CHANGELOG.md ├── src ├── main.rs ├── lib.rs └── cli.rs ├── README.md ├── LICENSE-APACHE └── Cargo.lock /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | -------------------------------------------------------------------------------- /tests/cli-help.rs: -------------------------------------------------------------------------------- 1 | use snapbox::cmd::{cargo_bin, Command}; 2 | 3 | #[test] 4 | fn long() { 5 | Command::new(cargo_bin!("vsp-router")) 6 | .arg("--help") 7 | .assert() 8 | .success() 9 | .stderr_eq("") 10 | .stdout_matches_path("tests/snapshots/cli-help-long.txt"); 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Rob Donnelly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /RELEASE.adoc: -------------------------------------------------------------------------------- 1 | = Release Process 2 | 3 | This project uses https://opensource.axo.dev/cargo-dist/[cargo-dist] and https://github.com/crate-ci/cargo-release[cargo-release] to simplify cutting new releases. 4 | 5 | == Update the Changelog 6 | 7 | vim CHANGELOG.md 8 | git add CHANGELOG.md 9 | git commit 10 | git push 11 | 12 | == Dryrun the Release 13 | 14 | cargo dist build 15 | cargo dist plan 16 | cargo release 17 | 18 | E.g. 19 | 20 | cargo release 1.0.1 21 | 22 | == Execute the Release 23 | 24 | cargo release --execute 25 | 26 | E.g. 27 | 28 | cargo release 1.0.1 --execute 29 | 30 | This will: 31 | 32 | * Update version in `Cargo.toml` and `Cargo.lock` and commit them 33 | * Publish the release to crates.io 34 | * Creates a tag and pushes it 35 | 36 | Pushing the tag then kicks off cargo-dist which: 37 | 38 | * Builds release artifacts 39 | * Creates a GitHub Release 40 | 41 | == Sit Back and Relax 42 | 43 | Sit back and relax while `cargo-dist` automatically creates a GitHub Release and releases binaries for each platform. 44 | 45 | == References 46 | 47 | See the https://github.com/axodotdev/cargo-dist/blob/main/book/src/workspaces/cargo-release-guide.md[cargo-dist cargo-release guide]. 48 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "vsp-router" 3 | version = "1.0.3" 4 | edition = "2021" 5 | description = "Create virtual serial ports, connect them to physical serial ports, and create routes between them all." 6 | authors = ["Rob Donnelly"] 7 | license = "MIT OR Apache-2.0" 8 | repository = "https://github.com/rfdonnelly/vsp-router" 9 | keywords = ["serial", "socat", "ttybus", "virtual", "router"] 10 | categories = ["command-line-utilities"] 11 | 12 | [profile.release] 13 | strip = true 14 | 15 | # The profile that 'cargo dist' will build with 16 | [profile.dist] 17 | inherits = "release" 18 | lto = "thin" 19 | 20 | [dependencies] 21 | anyhow = "1" 22 | bytes = "1" 23 | camino = "1" 24 | clap = { version = "4", features = ["derive"] } 25 | color-print = "0.3" 26 | futures = "0.3" 27 | futures-util = "0.3" 28 | thiserror = "1" 29 | tokio = { version = "1", features = ["full"] } 30 | tokio-serial = "5" 31 | tokio-stream = "0.1" 32 | tokio-util = { version = "0.7", features = ["io"] } 33 | tracing = "0.1" 34 | tracing-subscriber = "0.3" 35 | 36 | [dev-dependencies] 37 | snapbox = { version = "0.4.0", features = ["cmd"] } 38 | 39 | # Config for 'cargo dist' 40 | [workspace.metadata.dist] 41 | # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) 42 | cargo-dist-version = "0.21.0" 43 | # CI backends to support 44 | ci = "github" 45 | # The installers to generate for each app 46 | installers = [] 47 | # Target platforms to build apps for (Rust target-triple syntax) 48 | targets = ["aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-pc-windows-msvc"] 49 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: ci 4 | 5 | jobs: 6 | check: 7 | name: Check 8 | strategy: 9 | fail-fast: false 10 | matrix: 11 | include: 12 | - os: ubuntu-20.04 13 | - os: macos-11 14 | - os: windows-2019 15 | runs-on: ${{ matrix.os }} 16 | steps: 17 | - uses: actions/checkout@v2 18 | - uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | - uses: actions-rs/cargo@v1 24 | with: 25 | command: check 26 | 27 | test: 28 | name: Test Suite 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v2 32 | - uses: actions-rs/toolchain@v1 33 | with: 34 | profile: minimal 35 | toolchain: stable 36 | override: true 37 | - uses: actions-rs/cargo@v1 38 | with: 39 | command: test 40 | 41 | fmt: 42 | name: Rustfmt 43 | runs-on: ubuntu-latest 44 | steps: 45 | - uses: actions/checkout@v2 46 | - uses: actions-rs/toolchain@v1 47 | with: 48 | profile: minimal 49 | toolchain: stable 50 | override: true 51 | - run: rustup component add rustfmt 52 | - uses: actions-rs/cargo@v1 53 | with: 54 | command: fmt 55 | args: --all -- --check 56 | 57 | clippy: 58 | name: Clippy 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v2 62 | - uses: actions-rs/toolchain@v1 63 | with: 64 | profile: minimal 65 | toolchain: stable 66 | override: true 67 | - run: rustup component add clippy 68 | - uses: actions-rs/cargo@v1 69 | with: 70 | command: clippy 71 | args: -- -D warnings 72 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | No unrelease changes. 11 | 12 | ## Version 1.0.3 (2024-08-15) 13 | 14 | [GitHub Release page](https://github.com/rfdonnelly/vsp-router/releases/tag/v1.0.3) 15 | 16 | ### Fixed 17 | 18 | - Fixed blocking on full virtual serial port (PTY) 19 | 20 | Previous to this fix, vsp-router would block if a virtual serial port buffer became full. 21 | Now, vsp-router drops the data. 22 | See [#16](https://github.com/rfdonnelly/vsp-router/pull/16) for more details. 23 | 24 | ## Version 1.0.2 (YANKED) 25 | 26 | ## Version 1.0.1 (2023-08-01) 27 | 28 | [GitHub Release page](https://github.com/rfdonnelly/vsp-router/releases/tag/v1.0.1) 29 | 30 | ### Fixed 31 | 32 | - Fixed typo in README 33 | 34 | ## Version 1.0.0 (2023-08-01) 35 | 36 | [GitHub Release page](https://github.com/rfdonnelly/vsp-router/releases/tag/v1.0.0) 37 | 38 | ### Changed 39 | 40 | - Renamed the `--virtual` option to `--create` 41 | - Renamed the `--physical` option to `--attach` 42 | 43 | ## Version 0.3.0 (2023-08-01) 44 | 45 | [GitHub Release page](https://github.com/rfdonnelly/vsp-router/releases/tag/v0.3.0) 46 | 47 | ### Added 48 | 49 | - Added Windows support w/o ability to create virtual serial ports 50 | - Added Windows binaries 51 | 52 | ### Fixed 53 | 54 | - Fixed typo in `--help` 55 | 56 | ## Version 0.2.1 (2023-08-01) 57 | 58 | [GitHub Release page](https://github.com/rfdonnelly/vsp-router/releases/tag/v0.2.1) 59 | 60 | ### Added 61 | 62 | - Release automation and release binaries for Linux and macOS 63 | 64 | ## Version 0.2.0 (2022-10-29) 65 | 66 | [GitHub Release page](https://github.com/rfdonnelly/vsp-router/releases/tag/v0.2.0) 67 | 68 | ### Changed 69 | 70 | - Upgraded from clap v3 to clap v4 71 | - Replaced `tokio::select!` + `tokio_util::sync::CancellationToken` with `futures_util::future::Abortable` 72 | 73 | ## Version 0.1.1 (2022-09-11) 74 | 75 | [GitHub Release page](https://github.com/rfdonnelly/vsp-router/releases/tag/v0.1.1) 76 | 77 | Minor fixes for publishing on Crates.io. 78 | 79 | ## Version 0.1.0 (2022-09-10) 80 | 81 | [GitHub Release page](https://github.com/rfdonnelly/vsp-router/releases/tag/v0.1.0) 82 | 83 | Initial release. 84 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | 3 | use crate::cli::Cli; 4 | 5 | #[cfg(unix)] 6 | use vsp_router::create_virtual_serial_port; 7 | 8 | use vsp_router::{open_physical_serial_port, transfer}; 9 | 10 | use clap::Parser; 11 | use futures_util::future::{AbortHandle, Abortable, Aborted}; 12 | use tokio_stream::StreamMap; 13 | use tokio_util::io::ReaderStream; 14 | use tracing::{error, info}; 15 | 16 | use std::collections::HashMap; 17 | 18 | type AppError = anyhow::Error; 19 | type AppResult = anyhow::Result; 20 | 21 | #[tokio::main] 22 | async fn main() -> AppResult<()> { 23 | tracing_subscriber::fmt::init(); 24 | 25 | let args = Cli::parse(); 26 | args.validate()?; 27 | // TODO: Warn on non-routed sources 28 | 29 | let mut sources = StreamMap::new(); 30 | let mut sinks = HashMap::new(); 31 | #[cfg(unix)] 32 | let mut links = Vec::new(); 33 | 34 | #[cfg(unix)] 35 | for virtual_ in args.virtuals { 36 | let (port, link) = create_virtual_serial_port(&virtual_.path)?; 37 | let (reader, writer) = tokio::io::split(port); 38 | sources.insert(virtual_.id.clone(), ReaderStream::new(reader)); 39 | sinks.insert(virtual_.id.clone(), writer); 40 | links.push(link); 41 | } 42 | 43 | for physical in args.physicals { 44 | let port = open_physical_serial_port(&physical.path, physical.baud_rate)?; 45 | let (reader, writer) = tokio::io::split(port); 46 | sources.insert(physical.id.clone(), ReaderStream::new(reader)); 47 | sinks.insert(physical.id.clone(), writer); 48 | } 49 | 50 | let mut routes: HashMap> = HashMap::new(); 51 | for route in args.routes { 52 | routes.entry(route.src).or_default().push(route.dst); 53 | } 54 | info!(?routes); 55 | 56 | let (abort_handle, abort_registration) = AbortHandle::new_pair(); 57 | 58 | tokio::spawn(async move { 59 | match tokio::signal::ctrl_c().await { 60 | Ok(()) => info!("received ctrl-c, shutting down"), 61 | Err(e) => error!(?e, "unable to listen for shutdown signal"), 62 | } 63 | 64 | abort_handle.abort(); 65 | info!("waiting for graceful shutdown"); 66 | }); 67 | 68 | let abort_result = Abortable::new(transfer(sources, sinks, routes), abort_registration).await; 69 | match abort_result { 70 | Ok(transfer_result) => transfer_result?, 71 | Err(Aborted) => {} 72 | } 73 | 74 | Ok(()) 75 | } 76 | -------------------------------------------------------------------------------- /tests/snapshots/cli-help-long.txt: -------------------------------------------------------------------------------- 1 | Create virtual serial ports, connect them to physical serial ports, and create routes between them all. 2 | 3 | Usage: vsp-router [OPTIONS] 4 | 5 | Options: 6 | --create 7 | Create a virtual serial port. 8 | 9 | NOTE: This option is only applicable on POSIX platforms. This option is 10 | not applicable on Windows. 11 | 12 | The argument takes the following form: '[:]' 13 | 14 | If no ID is specified, the ID is set to the basename of the path. 15 | 16 | Can use multiple times to create multiple virtual serial ports. 17 | 18 | Examples: 19 | 20 | --create path/to/file 21 | 22 | The path is 'path/to/file' and the ID is 'file'. 23 | 24 | --create 0:dev/ttyUSB0 25 | 26 | The path is '/dev/ttyUSB0' and the ID is '0'. 27 | 28 | --attach 29 | Attach to an existing serial port. 30 | 31 | The argument takes the following form: '[:][,]' 32 | 33 | If ID is not specified, the ID is set to the basename of the path. If baud rate is not specified, 34 | the baud rate defaults to 9600. 35 | 36 | Can use multiple times to attach multiple serial ports. 37 | 38 | Examples: 39 | 40 | --attach /dev/ttyUSB0 41 | 42 | The path is '/dev/ttyUSB0', the ID is 'ttyUSB0', and the baud rate is 9600. 43 | 44 | --attach 1:/dev/ttyUSB0 45 | 46 | The path is '/dev/ttyUSB0', the ID is '1', and the baud rate is 9600. 47 | 48 | --attach 1:/dev/ttyUSB0,115200 49 | 50 | The path is '/dev/ttyUSB0', the ID is '1', and the baud rate is 115200. 51 | 52 | --route 53 | Create a route between a source port and a destination port. 54 | 55 | The argument takes the following form: ':' 56 | 57 | Can use multiple times to create multiple routes. 58 | 59 | Examples: 60 | 61 | --route 0:1 62 | 63 | The source ID is '0' and the destination ID is '1'. 64 | 65 | -h, --help 66 | Print help (see a summary with '-h') 67 | 68 | -V, --version 69 | Print version 70 | 71 | 72 | Examples: 73 | 74 | Share a physical serial port with two virtual serial ports. 75 | 76 | Data sent from virtual serial port 0 is sent to the physical serial port but not to virtual 77 | serial port 1. Similarly, data sent from virtual serial port 1 is sent to the physical serial 78 | port but not to virtual serial port 0. Data received from the physical serial port is sent to 79 | both virtual serial ports. 80 | 81 | vsp-router / 82 | --create 0 / 83 | --create 1 / 84 | --attach 2:/dev/ttyUSB0,115200 / 85 | --route 0:2 / 86 | --route 1:2 / 87 | --route 2:0 / 88 | --route 2:1 89 | -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | use vsp_router::{create_virtual_serial_port, transfer}; 2 | 3 | use bytes::{Bytes, BytesMut}; 4 | use futures_util::future::{AbortHandle, Abortable}; 5 | use tokio::io::{AsyncReadExt, AsyncWriteExt}; 6 | use tokio::time::timeout; 7 | use tokio_serial::SerialPortBuilderExt; 8 | use tokio_stream::StreamMap; 9 | use tokio_util::io::ReaderStream; 10 | 11 | use std::collections::HashMap; 12 | use std::time::Duration; 13 | 14 | #[tokio::test] 15 | async fn virtual_routes() { 16 | // let _ = tracing_subscriber::fmt::try_init(); 17 | 18 | let baud_rate = 115200; 19 | 20 | let (sources, (sinks, _symlinks)): (Vec<_>, (Vec<_>, Vec<_>)) = (0..3) 21 | .map(|id| { 22 | let (port, symlink) = create_virtual_serial_port(id.to_string()).unwrap(); 23 | let (reader, writer) = tokio::io::split(port); 24 | let reader_stream = ReaderStream::new(reader); 25 | (reader_stream, (writer, symlink)) 26 | }) 27 | .unzip(); 28 | let sources: StreamMap> = sources 29 | .into_iter() 30 | .enumerate() 31 | .map(|(id, source)| (id.to_string(), source)) 32 | .collect(); 33 | let sinks: HashMap = sinks 34 | .into_iter() 35 | .enumerate() 36 | .map(|(id, sink)| (id.to_string(), sink)) 37 | .collect(); 38 | 39 | let mut end_points = (0..3) 40 | .map(|id| { 41 | tokio_serial::new(id.to_string(), baud_rate) 42 | .open_native_async() 43 | .unwrap() 44 | }) 45 | .collect::>(); 46 | 47 | let routes: HashMap> = [ 48 | ("0".to_owned(), vec!["2".to_owned()]), 49 | ("1".to_owned(), vec!["2".to_owned()]), 50 | ("2".to_owned(), vec!["0".to_owned(), "1".to_owned()]), 51 | ] 52 | .into_iter() 53 | .collect(); 54 | 55 | let (abort_handle, abort_registration) = AbortHandle::new_pair(); 56 | 57 | let join_handle = tokio::spawn(async move { 58 | Abortable::new(transfer(sources, sinks, routes), abort_registration) 59 | .await 60 | .map(|transfer_result| transfer_result.unwrap()) 61 | .ok() 62 | }); 63 | 64 | timeout(Duration::from_secs(1), async move { 65 | for _ in 0..2 { 66 | let msg = Bytes::from("from 0"); 67 | end_points[0].write_all(&msg).await.unwrap(); 68 | let mut buf = BytesMut::new(); 69 | end_points[2].read_buf(&mut buf).await.unwrap(); 70 | assert_eq!(msg, buf); 71 | 72 | let msg = Bytes::from("from 1"); 73 | end_points[1].write_all(&msg).await.unwrap(); 74 | let mut buf = BytesMut::new(); 75 | end_points[2].read_buf(&mut buf).await.unwrap(); 76 | assert_eq!(msg, buf); 77 | 78 | let msg = Bytes::from("from 2"); 79 | end_points[2].write_all(&msg).await.unwrap(); 80 | let mut buf = BytesMut::new(); 81 | end_points[0].read_buf(&mut buf).await.unwrap(); 82 | assert_eq!(msg, buf); 83 | let mut buf = BytesMut::new(); 84 | end_points[1].read_buf(&mut buf).await.unwrap(); 85 | assert_eq!(msg, buf); 86 | } 87 | }) 88 | .await 89 | .expect("test took longer than expected"); 90 | 91 | abort_handle.abort(); 92 | join_handle.await.unwrap(); 93 | } 94 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Buf, Bytes}; 2 | use camino::{Utf8Path, Utf8PathBuf}; 3 | use thiserror::Error; 4 | use tokio::io::{AsyncRead, AsyncWrite}; 5 | #[cfg(unix)] 6 | use tokio_serial::SerialPort; 7 | use tokio_serial::SerialPortBuilderExt; 8 | use tokio_serial::SerialStream; 9 | use tokio_stream::{StreamExt, StreamMap}; 10 | use tokio_util::io::ReaderStream; 11 | use tracing::{error, info, warn}; 12 | 13 | use std::collections::HashMap; 14 | use std::fs; 15 | use std::io::ErrorKind; 16 | use std::pin::Pin; 17 | use std::task::Poll::Ready; 18 | 19 | #[cfg(unix)] 20 | use std::os::unix; 21 | 22 | #[derive(Error, Debug)] 23 | pub enum Error { 24 | #[error("could not create link to pty")] 25 | Link(#[source] std::io::Error), 26 | 27 | #[error("serial error")] 28 | Serial(#[source] tokio_serial::Error), 29 | 30 | #[error("stream closed")] 31 | Closed, 32 | 33 | #[error("read error")] 34 | Read(#[source] std::io::Error), 35 | 36 | #[error("write error")] 37 | Write(#[source] std::io::Error), 38 | } 39 | 40 | pub struct PtyLink { 41 | // Not used directly but need to keep around to prevent early close of the file descriptor. 42 | // 43 | // tokio_serial::SerialStream includes a mio_serial::SerialStream which includes a 44 | // serialport::TTY which includes a Drop impl that closes the file descriptor. 45 | _subordinate: SerialStream, 46 | link: Utf8PathBuf, 47 | } 48 | 49 | pub type Result = std::result::Result; 50 | 51 | #[cfg(unix)] 52 | pub fn create_virtual_serial_port

(path: P) -> Result<(SerialStream, PtyLink)> 53 | where 54 | P: AsRef, 55 | { 56 | let (manager, subordinate) = SerialStream::pair().map_err(Error::Serial)?; 57 | let link = PtyLink::new(subordinate, path)?; 58 | 59 | Ok((manager, link)) 60 | } 61 | 62 | pub fn open_physical_serial_port

(path: P, baud_rate: u32) -> Result 63 | where 64 | P: AsRef, 65 | { 66 | tokio_serial::new(path.as_ref().as_str(), baud_rate) 67 | .open_native_async() 68 | .map_err(Error::Serial) 69 | } 70 | 71 | #[cfg(unix)] 72 | impl PtyLink { 73 | fn new>(subordinate: SerialStream, path: P) -> Result { 74 | let link = path.as_ref().to_path_buf(); 75 | unix::fs::symlink(subordinate.name().unwrap(), link.as_std_path()).map_err(Error::Link)?; 76 | 77 | Ok(PtyLink { 78 | _subordinate: subordinate, 79 | link, 80 | }) 81 | } 82 | 83 | pub fn link(&self) -> &Utf8Path { 84 | self.link.as_path() 85 | } 86 | 87 | pub fn id(&self) -> &str { 88 | self.link.as_str() 89 | } 90 | } 91 | 92 | impl Drop for PtyLink { 93 | fn drop(&mut self) { 94 | if fs::remove_file(&self.link).is_err() { 95 | eprintln!("error: could not delete {}", self.link); 96 | } 97 | } 98 | } 99 | 100 | #[tracing::instrument(skip_all)] 101 | pub async fn transfer( 102 | mut sources: StreamMap>, 103 | mut sinks: HashMap, 104 | routes: HashMap>, 105 | ) -> Result<()> 106 | where 107 | R: AsyncRead + Unpin, 108 | W: AsyncWrite + Unpin, 109 | { 110 | while let Some((src_id, result)) = sources.next().await { 111 | if let Some(dst_ids) = routes.get(&src_id) { 112 | let bytes = result.map_err(Error::Read)?; 113 | info!(?src_id, ?dst_ids, ?bytes, "read"); 114 | for dst_id in dst_ids { 115 | if let Some(dst) = sinks.get_mut(dst_id) { 116 | let mut buf = bytes.clone(); 117 | if let Err(e) = write_non_blocking(dst, &mut buf).await { 118 | if let Error::Write(io_err) = &e { 119 | if io_err.kind() == ErrorKind::WouldBlock { 120 | warn!(?dst_id, ?bytes, "discarded"); 121 | } else { 122 | error!(?dst_id, ?e, "write error"); 123 | } 124 | } 125 | } else { 126 | info!(?dst_id, ?bytes, "wrote"); 127 | } 128 | } 129 | } 130 | } 131 | } 132 | 133 | Ok(()) 134 | } 135 | 136 | async fn write_non_blocking(dst: &mut W, buf: &mut Bytes) -> Result<()> { 137 | let waker = futures::task::noop_waker(); 138 | let mut cx = futures::task::Context::from_waker(&waker); 139 | 140 | let pinned_dst = Pin::new(dst); 141 | match pinned_dst.poll_write(&mut cx, buf) { 142 | Ready(Ok(bytes_written)) => { 143 | buf.advance(bytes_written); 144 | Ok(()) 145 | } 146 | Ready(Err(e)) => Err(Error::Write(e)), 147 | _ => Err(Error::Write(std::io::Error::new( 148 | ErrorKind::WouldBlock, 149 | "Would block", 150 | ))), 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Virtual Serial Port Router (vsp-router) 2 | 3 | [![Build status](https://github.com/rfdonnelly/vsp-router/workflows/ci/badge.svg)](https://github.com/rfdonnelly/vsp-router/actions) 4 | [![Crates.io](https://img.shields.io/crates/v/vsp-router.svg)](https://crates.io/crates/vsp-router) 5 | 6 | Create virtual serial ports, connect them to physical serial ports, and create routes between them all. 7 | 8 | Vsp-router was created to connect two terminal emulators to the same physical RS-232 [serial console](https://tldp.org/HOWTO/Remote-Serial-Console-HOWTO/intro-why.html). 9 | 10 | [![asciicast](https://asciinema.org/a/519137.svg)](https://asciinema.org/a/519137) 11 | 12 | ## Supported Operating Systems 13 | 14 | * Linux: Yes, tested on Red Hat Enterprise Linux 8 15 | * macOS: Yes, tested on macOS Ventura 13.1 16 | * Windows: Yes*, tested on Windows 10 17 | 18 | *The Windows version does not support creation of virtual serial ports. 19 | A third-party tool like [com0com](https://com0com.sourceforge.net) can be used instead. 20 | 21 | ## Use Cases 22 | 23 | Multiplex two virtual serial ports to a single physical serial port. 24 | 25 | ```sh 26 | vsp-router \ 27 | --create 0 \ 28 | --create 1 \ 29 | --attach 2:/dev/ttyUSB0 \ 30 | --route 0:2 \ 31 | --route 1:2 \ 32 | --route 2:0 \ 33 | --route 2:1 34 | ``` 35 | 36 | Multiplex two virtual serial ports to a third virtual serial port. 37 | 38 | ```sh 39 | vsp-router \ 40 | --create 0 \ 41 | --create 1 \ 42 | --create 2 \ 43 | --route 0:2 \ 44 | --route 1:2 \ 45 | --route 2:0 \ 46 | --route 2:1 47 | ``` 48 | 49 | ## Example 50 | 51 | In terminal A 52 | 53 | ```sh 54 | cargo run -- \ 55 | --create 0 \ 56 | --create 1 \ 57 | --create 2 \ 58 | --route 0:2 \ 59 | --route 1:2 \ 60 | --route 2:0 \ 61 | --route 2:1 62 | ``` 63 | 64 | In terminal 0 65 | 66 | ```sh 67 | picocom 0 68 | ``` 69 | 70 | In terminal 1 71 | 72 | ```sh 73 | picocom 1 74 | ``` 75 | 76 | In terminal 2 77 | 78 | ```sh 79 | picocom 2 80 | ``` 81 | 82 | Characters entered in terminal 0 will be sent to terminal 2 only.\ 83 | Characters entered in terminal 1 will be sent to terminal 2 only.\ 84 | Characters entered in terminal 2 will be sent to both terminals 0 and 1. 85 | 86 | ## Connection to a Virtual Serial Port 87 | 88 | Virtual serial ports created by vsp-router behave a bit differently from physical serial ports: 89 | 90 | * You can connect to them using any baud rate. 91 | You are not forced to use the same baud rate as the physical serial port you are multiplexing. 92 | * When you don't read from them they accumulate data in a buffer. 93 | When this buffer becomes full new data will be discarded and a warning message will be shown in the logs. 94 | Any buffered data will get returned when you next read from them. 95 | 96 | To avoid reading stale data accumulated in the buffer when you want to read from the virtual serial port it is recommended to flush its input buffer before you first read from it. 97 | This can be done using `tcflush(fd, TCIFLUSH)` (or equivalent on your platform) on the file descriptor of the virtual serial port. 98 | 99 | ## Comparison to TTYBUS 100 | 101 | vsp-router is similar to [TTYBUS](https://github.com/danielinux/ttybus). 102 | 103 | The key differences is in how data is written. 104 | TTYBUS broadcasts data to all ports. 105 | vsp-router routes data to select ports. 106 | 107 | The following 3-port configurations are the equivalent. 108 | 109 | TTYBUS 110 | 111 | ```sh 112 | tty_bus -d -s bus 113 | tty_fake -d -s bus 0 114 | tty_fake -d -s bus 1 115 | tty_fake -d -s bus 2 116 | ``` 117 | 118 | vsp-router 119 | 120 | ```sh 121 | vsp-router \ 122 | --create 0 \ 123 | --create 1 \ 124 | --create 2 \ 125 | --route 0:1 \ 126 | --route 0:2 \ 127 | --route 1:0 \ 128 | --route 1:2 \ 129 | --route 2:0 \ 130 | --route 2:1 131 | ``` 132 | 133 | ## Comparison to socat 134 | 135 | Socat establishes a bidirectional channel between two end points. 136 | Vsp-router establishes a multi-directional channel between N end points. 137 | Socat supports several different types of end points. 138 | Vsp-router supports two: virtual serial ports and physical serial ports. 139 | 140 | Vsp-router could be combined with socat to enable interesting use cases. 141 | For example, vsp-router could be used to snoop a physical serial port and paired with socat to send the snooped data over UDP. 142 | 143 | ## License 144 | 145 | Licensed under either of 146 | 147 | * Apache License, Version 2.0 148 | ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) 149 | * MIT license 150 | ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) 151 | 152 | at your option. 153 | 154 | ## Contribution 155 | 156 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. 157 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use crate::{AppError, AppResult}; 2 | 3 | use anyhow::anyhow; 4 | use camino::Utf8PathBuf; 5 | use clap::Parser; 6 | 7 | use std::collections::HashMap; 8 | use std::str::FromStr; 9 | 10 | #[derive(Parser)] 11 | #[command(author, version, about, after_help = CLAP_AFTER_HELP)] 12 | pub(crate) struct Cli { 13 | /// Create a virtual serial port. 14 | /// 15 | /// NOTE: This option is only applicable on POSIX platforms. This option is 16 | /// not applicable on Windows. 17 | /// 18 | /// The argument takes the following form: '[:]' 19 | /// 20 | /// If no ID is specified, the ID is set to the basename of the path. 21 | /// 22 | /// Can use multiple times to create multiple virtual serial ports. 23 | /// 24 | /// Examples: 25 | /// 26 | /// --create path/to/file 27 | /// 28 | /// The path is 'path/to/file' and the ID is 'file'. 29 | /// 30 | /// --create 0:dev/ttyUSB0 31 | /// 32 | /// The path is '/dev/ttyUSB0' and the ID is '0'. 33 | #[arg(long = "create", id = "CREATE", verbatim_doc_comment)] 34 | pub(crate) virtuals: Vec, 35 | 36 | /// Attach to an existing serial port. 37 | /// 38 | /// The argument takes the following form: '[:][,]' 39 | /// 40 | /// If ID is not specified, the ID is set to the basename of the path. If baud rate is not specified, 41 | /// the baud rate defaults to 9600. 42 | /// 43 | /// Can use multiple times to attach multiple serial ports. 44 | /// 45 | /// Examples: 46 | /// 47 | /// --attach /dev/ttyUSB0 48 | /// 49 | /// The path is '/dev/ttyUSB0', the ID is 'ttyUSB0', and the baud rate is 9600. 50 | /// 51 | /// --attach 1:/dev/ttyUSB0 52 | /// 53 | /// The path is '/dev/ttyUSB0', the ID is '1', and the baud rate is 9600. 54 | /// 55 | /// --attach 1:/dev/ttyUSB0,115200 56 | /// 57 | /// The path is '/dev/ttyUSB0', the ID is '1', and the baud rate is 115200. 58 | #[arg(long = "attach", id = "ATTACH", verbatim_doc_comment)] 59 | pub(crate) physicals: Vec, 60 | 61 | /// Create a route between a source port and a destination port. 62 | /// 63 | /// The argument takes the following form: ':' 64 | /// 65 | /// Can use multiple times to create multiple routes. 66 | /// 67 | /// Examples: 68 | /// 69 | /// --route 0:1 70 | /// 71 | /// The source ID is '0' and the destination ID is '1'. 72 | #[arg(long = "route", id = "ROUTE", verbatim_doc_comment)] 73 | pub(crate) routes: Vec, 74 | } 75 | 76 | const CLAP_AFTER_HELP: &str = color_print::cstr!( 77 | " 78 | Examples: 79 | 80 | Share a physical serial port with two virtual serial ports. 81 | 82 | Data sent from virtual serial port 0 is sent to the physical serial port but not to virtual 83 | serial port 1. Similarly, data sent from virtual serial port 1 is sent to the physical serial 84 | port but not to virtual serial port 0. Data received from the physical serial port is sent to 85 | both virtual serial ports. 86 | 87 | vsp-router \\ 88 | --create 0 \\ 89 | --create 1 \\ 90 | --attach 2:/dev/ttyUSB0,115200 \\ 91 | --route 0:2 \\ 92 | --route 1:2 \\ 93 | --route 2:0 \\ 94 | --route 2:1 95 | " 96 | ); 97 | 98 | #[derive(Clone, Debug)] 99 | pub(crate) struct Virtual { 100 | pub(crate) id: String, 101 | pub(crate) path: Utf8PathBuf, 102 | } 103 | 104 | #[derive(Clone, Debug)] 105 | pub(crate) struct Physical { 106 | pub(crate) id: String, 107 | pub(crate) path: Utf8PathBuf, 108 | pub(crate) baud_rate: u32, 109 | } 110 | 111 | #[derive(Clone, Debug)] 112 | pub(crate) struct Route { 113 | pub(crate) src: String, 114 | pub(crate) dst: String, 115 | } 116 | 117 | impl Cli { 118 | pub(crate) fn validate(&self) -> AppResult<()> { 119 | self.check_windows_virtuals()?; 120 | self.check_duplicate_ids()?; 121 | self.check_route_ids() 122 | } 123 | 124 | fn check_windows_virtuals(&self) -> AppResult<()> { 125 | #[cfg(not(unix))] 126 | if !self.virtuals.is_empty() { 127 | Err(anyhow!("the --virtual option is not available on Windows")) 128 | } else { 129 | Ok(()) 130 | } 131 | 132 | #[cfg(unix)] 133 | Ok(()) 134 | } 135 | 136 | fn ids(&self) -> impl Iterator { 137 | #[cfg(unix)] 138 | let virtual_ids = self.virtuals.iter().map(|virtual_| virtual_.id.as_str()); 139 | #[cfg(not(unix))] 140 | let virtual_ids = { 141 | let virtual_ids: &[String] = &[]; 142 | virtual_ids.iter().map(|virtual_id| virtual_id.as_str()) 143 | }; 144 | let physical_ids = self.physicals.iter().map(|physical| physical.id.as_str()); 145 | 146 | virtual_ids.chain(physical_ids) 147 | } 148 | 149 | fn check_route_ids(&self) -> AppResult<()> { 150 | let ids = self.ids().collect::>(); 151 | 152 | for route in &self.routes { 153 | if !ids.contains(&route.src.as_str()) { 154 | Err(anyhow!( 155 | "the source ID '{}' in route '{}:{}' was not found", 156 | route.src, 157 | route.src, 158 | route.dst 159 | ))?; 160 | } 161 | if !ids.contains(&route.dst.as_str()) { 162 | Err(anyhow!( 163 | "the destination ID '{}' in route '{}:{}' was not found", 164 | route.dst, 165 | route.src, 166 | route.dst 167 | ))?; 168 | } 169 | } 170 | 171 | Ok(()) 172 | } 173 | 174 | fn check_duplicate_ids(&self) -> AppResult<()> { 175 | let duplicate_ids = self 176 | .ids() 177 | .fold(HashMap::new(), |mut map, id| { 178 | *map.entry(id).or_insert(0) += 1; 179 | map 180 | }) 181 | .iter() 182 | .filter_map(|(&id, &count)| if count > 1 { Some(id) } else { None }) 183 | .collect::>(); 184 | 185 | if !duplicate_ids.is_empty() { 186 | Err(anyhow!( 187 | "the following IDs were used more than once: {}", 188 | duplicate_ids.join(", ") 189 | )) 190 | } else { 191 | Ok(()) 192 | } 193 | } 194 | } 195 | 196 | impl FromStr for Virtual { 197 | type Err = AppError; 198 | 199 | fn from_str(s: &str) -> Result { 200 | match s.split_once(':') { 201 | None => { 202 | let path = Utf8PathBuf::from(s); 203 | let id = path 204 | .file_name() 205 | .ok_or_else(|| anyhow!("invalid path '{s}'"))? 206 | .to_owned(); 207 | Ok(Self { id, path }) 208 | } 209 | Some((id, path)) => { 210 | let id = id.to_owned(); 211 | let path = Utf8PathBuf::from(path); 212 | Ok(Self { id, path }) 213 | } 214 | } 215 | } 216 | } 217 | 218 | impl FromStr for Physical { 219 | type Err = AppError; 220 | 221 | fn from_str(s: &str) -> Result { 222 | let (remainder, baud_rate) = match s.split_once(',') { 223 | None => (s, 9600), 224 | Some((remainder, baud_rate)) => { 225 | let baud_rate = baud_rate.parse()?; 226 | (remainder, baud_rate) 227 | } 228 | }; 229 | 230 | let id_path = Virtual::from_str(remainder)?; 231 | 232 | Ok(Physical { 233 | id: id_path.id, 234 | path: id_path.path, 235 | baud_rate, 236 | }) 237 | } 238 | } 239 | 240 | impl FromStr for Route { 241 | type Err = AppError; 242 | 243 | fn from_str(s: &str) -> Result { 244 | let (src, dst) = s 245 | .split_once(':') 246 | .ok_or_else(|| anyhow!("invalid route '{s}'"))?; 247 | Ok(Self { 248 | src: src.to_string(), 249 | dst: dst.to_string(), 250 | }) 251 | } 252 | } 253 | 254 | #[cfg(test)] 255 | mod test { 256 | use super::*; 257 | 258 | #[test] 259 | fn debug_assert() { 260 | use clap::CommandFactory; 261 | Cli::command().debug_assert(); 262 | } 263 | } 264 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2022 Rob Donnelly 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # Copyright 2022-2024, axodotdev 2 | # SPDX-License-Identifier: MIT or Apache-2.0 3 | # 4 | # CI that: 5 | # 6 | # * checks for a Git Tag that looks like a release 7 | # * builds artifacts with cargo-dist (archives, installers, hashes) 8 | # * uploads those artifacts to temporary workflow zip 9 | # * on success, uploads the artifacts to a GitHub Release 10 | # 11 | # Note that the GitHub Release will be created with a generated 12 | # title/body based on your changelogs. 13 | 14 | name: Release 15 | permissions: 16 | "contents": "write" 17 | 18 | # This task will run whenever you push a git tag that looks like a version 19 | # like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. 20 | # Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where 21 | # PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION 22 | # must be a Cargo-style SemVer Version (must have at least major.minor.patch). 23 | # 24 | # If PACKAGE_NAME is specified, then the announcement will be for that 25 | # package (erroring out if it doesn't have the given version or isn't cargo-dist-able). 26 | # 27 | # If PACKAGE_NAME isn't specified, then the announcement will be for all 28 | # (cargo-dist-able) packages in the workspace with that version (this mode is 29 | # intended for workspaces with only one dist-able package, or with all dist-able 30 | # packages versioned/released in lockstep). 31 | # 32 | # If you push multiple tags at once, separate instances of this workflow will 33 | # spin up, creating an independent announcement for each one. However, GitHub 34 | # will hard limit this to 3 tags per commit, as it will assume more tags is a 35 | # mistake. 36 | # 37 | # If there's a prerelease-style suffix to the version, then the release(s) 38 | # will be marked as a prerelease. 39 | on: 40 | pull_request: 41 | push: 42 | tags: 43 | - '**[0-9]+.[0-9]+.[0-9]+*' 44 | 45 | jobs: 46 | # Run 'cargo dist plan' (or host) to determine what tasks we need to do 47 | plan: 48 | runs-on: "ubuntu-20.04" 49 | outputs: 50 | val: ${{ steps.plan.outputs.manifest }} 51 | tag: ${{ !github.event.pull_request && github.ref_name || '' }} 52 | tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} 53 | publishing: ${{ !github.event.pull_request }} 54 | env: 55 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | steps: 57 | - uses: actions/checkout@v4 58 | with: 59 | submodules: recursive 60 | - name: Install cargo-dist 61 | # we specify bash to get pipefail; it guards against the `curl` command 62 | # failing. otherwise `sh` won't catch that `curl` returned non-0 63 | shell: bash 64 | run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.21.0/cargo-dist-installer.sh | sh" 65 | - name: Cache cargo-dist 66 | uses: actions/upload-artifact@v4 67 | with: 68 | name: cargo-dist-cache 69 | path: ~/.cargo/bin/cargo-dist 70 | # sure would be cool if github gave us proper conditionals... 71 | # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible 72 | # functionality based on whether this is a pull_request, and whether it's from a fork. 73 | # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* 74 | # but also really annoying to build CI around when it needs secrets to work right.) 75 | - id: plan 76 | run: | 77 | cargo dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json 78 | echo "cargo dist ran successfully" 79 | cat plan-dist-manifest.json 80 | echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" 81 | - name: "Upload dist-manifest.json" 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: artifacts-plan-dist-manifest 85 | path: plan-dist-manifest.json 86 | 87 | # Build and packages all the platform-specific things 88 | build-local-artifacts: 89 | name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) 90 | # Let the initial task tell us to not run (currently very blunt) 91 | needs: 92 | - plan 93 | if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} 94 | strategy: 95 | fail-fast: false 96 | # Target platforms/runners are computed by cargo-dist in create-release. 97 | # Each member of the matrix has the following arguments: 98 | # 99 | # - runner: the github runner 100 | # - dist-args: cli flags to pass to cargo dist 101 | # - install-dist: expression to run to install cargo-dist on the runner 102 | # 103 | # Typically there will be: 104 | # - 1 "global" task that builds universal installers 105 | # - N "local" tasks that build each platform's binaries and platform-specific installers 106 | matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} 107 | runs-on: ${{ matrix.runner }} 108 | env: 109 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 110 | BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json 111 | steps: 112 | - name: enable windows longpaths 113 | run: | 114 | git config --global core.longpaths true 115 | - uses: actions/checkout@v4 116 | with: 117 | submodules: recursive 118 | - name: Install cargo-dist 119 | run: ${{ matrix.install_dist }} 120 | # Get the dist-manifest 121 | - name: Fetch local artifacts 122 | uses: actions/download-artifact@v4 123 | with: 124 | pattern: artifacts-* 125 | path: target/distrib/ 126 | merge-multiple: true 127 | - name: Install dependencies 128 | run: | 129 | ${{ matrix.packages_install }} 130 | - name: Build artifacts 131 | run: | 132 | # Actually do builds and make zips and whatnot 133 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json 134 | echo "cargo dist ran successfully" 135 | - id: cargo-dist 136 | name: Post-build 137 | # We force bash here just because github makes it really hard to get values up 138 | # to "real" actions without writing to env-vars, and writing to env-vars has 139 | # inconsistent syntax between shell and powershell. 140 | shell: bash 141 | run: | 142 | # Parse out what we just built and upload it to scratch storage 143 | echo "paths<> "$GITHUB_OUTPUT" 144 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 145 | echo "EOF" >> "$GITHUB_OUTPUT" 146 | 147 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 148 | - name: "Upload artifacts" 149 | uses: actions/upload-artifact@v4 150 | with: 151 | name: artifacts-build-local-${{ join(matrix.targets, '_') }} 152 | path: | 153 | ${{ steps.cargo-dist.outputs.paths }} 154 | ${{ env.BUILD_MANIFEST_NAME }} 155 | 156 | # Build and package all the platform-agnostic(ish) things 157 | build-global-artifacts: 158 | needs: 159 | - plan 160 | - build-local-artifacts 161 | runs-on: "ubuntu-20.04" 162 | env: 163 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 164 | BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json 165 | steps: 166 | - uses: actions/checkout@v4 167 | with: 168 | submodules: recursive 169 | - name: Install cached cargo-dist 170 | uses: actions/download-artifact@v4 171 | with: 172 | name: cargo-dist-cache 173 | path: ~/.cargo/bin/ 174 | - run: chmod +x ~/.cargo/bin/cargo-dist 175 | # Get all the local artifacts for the global tasks to use (for e.g. checksums) 176 | - name: Fetch local artifacts 177 | uses: actions/download-artifact@v4 178 | with: 179 | pattern: artifacts-* 180 | path: target/distrib/ 181 | merge-multiple: true 182 | - id: cargo-dist 183 | shell: bash 184 | run: | 185 | cargo dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json 186 | echo "cargo dist ran successfully" 187 | 188 | # Parse out what we just built and upload it to scratch storage 189 | echo "paths<> "$GITHUB_OUTPUT" 190 | jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" 191 | echo "EOF" >> "$GITHUB_OUTPUT" 192 | 193 | cp dist-manifest.json "$BUILD_MANIFEST_NAME" 194 | - name: "Upload artifacts" 195 | uses: actions/upload-artifact@v4 196 | with: 197 | name: artifacts-build-global 198 | path: | 199 | ${{ steps.cargo-dist.outputs.paths }} 200 | ${{ env.BUILD_MANIFEST_NAME }} 201 | # Determines if we should publish/announce 202 | host: 203 | needs: 204 | - plan 205 | - build-local-artifacts 206 | - build-global-artifacts 207 | # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine) 208 | if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} 209 | env: 210 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 211 | runs-on: "ubuntu-20.04" 212 | outputs: 213 | val: ${{ steps.host.outputs.manifest }} 214 | steps: 215 | - uses: actions/checkout@v4 216 | with: 217 | submodules: recursive 218 | - name: Install cached cargo-dist 219 | uses: actions/download-artifact@v4 220 | with: 221 | name: cargo-dist-cache 222 | path: ~/.cargo/bin/ 223 | - run: chmod +x ~/.cargo/bin/cargo-dist 224 | # Fetch artifacts from scratch-storage 225 | - name: Fetch artifacts 226 | uses: actions/download-artifact@v4 227 | with: 228 | pattern: artifacts-* 229 | path: target/distrib/ 230 | merge-multiple: true 231 | - id: host 232 | shell: bash 233 | run: | 234 | cargo dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json 235 | echo "artifacts uploaded and released successfully" 236 | cat dist-manifest.json 237 | echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" 238 | - name: "Upload dist-manifest.json" 239 | uses: actions/upload-artifact@v4 240 | with: 241 | # Overwrite the previous copy 242 | name: artifacts-dist-manifest 243 | path: dist-manifest.json 244 | # Create a GitHub Release while uploading all files to it 245 | - name: "Download GitHub Artifacts" 246 | uses: actions/download-artifact@v4 247 | with: 248 | pattern: artifacts-* 249 | path: artifacts 250 | merge-multiple: true 251 | - name: Cleanup 252 | run: | 253 | # Remove the granular manifests 254 | rm -f artifacts/*-dist-manifest.json 255 | - name: Create GitHub Release 256 | env: 257 | PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" 258 | ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" 259 | ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" 260 | RELEASE_COMMIT: "${{ github.sha }}" 261 | run: | 262 | # Write and read notes from a file to avoid quoting breaking things 263 | echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt 264 | 265 | gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* 266 | 267 | announce: 268 | needs: 269 | - plan 270 | - host 271 | # use "always() && ..." to allow us to wait for all publish jobs while 272 | # still allowing individual publish jobs to skip themselves (for prereleases). 273 | # "host" however must run to completion, no skipping allowed! 274 | if: ${{ always() && needs.host.result == 'success' }} 275 | runs-on: "ubuntu-20.04" 276 | env: 277 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 278 | steps: 279 | - uses: actions/checkout@v4 280 | with: 281 | submodules: recursive 282 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "addr2line" 7 | version = "0.22.0" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" 10 | dependencies = [ 11 | "gimli", 12 | ] 13 | 14 | [[package]] 15 | name = "adler" 16 | version = "1.0.2" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 19 | 20 | [[package]] 21 | name = "anstream" 22 | version = "0.6.15" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" 25 | dependencies = [ 26 | "anstyle", 27 | "anstyle-parse", 28 | "anstyle-query", 29 | "anstyle-wincon", 30 | "colorchoice", 31 | "is_terminal_polyfill", 32 | "utf8parse", 33 | ] 34 | 35 | [[package]] 36 | name = "anstyle" 37 | version = "1.0.8" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" 40 | 41 | [[package]] 42 | name = "anstyle-parse" 43 | version = "0.2.5" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" 46 | dependencies = [ 47 | "utf8parse", 48 | ] 49 | 50 | [[package]] 51 | name = "anstyle-query" 52 | version = "1.1.1" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" 55 | dependencies = [ 56 | "windows-sys 0.52.0", 57 | ] 58 | 59 | [[package]] 60 | name = "anstyle-wincon" 61 | version = "3.0.4" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" 64 | dependencies = [ 65 | "anstyle", 66 | "windows-sys 0.52.0", 67 | ] 68 | 69 | [[package]] 70 | name = "anyhow" 71 | version = "1.0.86" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" 74 | 75 | [[package]] 76 | name = "autocfg" 77 | version = "1.3.0" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 80 | 81 | [[package]] 82 | name = "backtrace" 83 | version = "0.3.73" 84 | source = "registry+https://github.com/rust-lang/crates.io-index" 85 | checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" 86 | dependencies = [ 87 | "addr2line", 88 | "cc", 89 | "cfg-if", 90 | "libc", 91 | "miniz_oxide", 92 | "object", 93 | "rustc-demangle", 94 | ] 95 | 96 | [[package]] 97 | name = "bitflags" 98 | version = "1.3.2" 99 | source = "registry+https://github.com/rust-lang/crates.io-index" 100 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 101 | 102 | [[package]] 103 | name = "bitflags" 104 | version = "2.6.0" 105 | source = "registry+https://github.com/rust-lang/crates.io-index" 106 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 107 | 108 | [[package]] 109 | name = "bytes" 110 | version = "1.7.1" 111 | source = "registry+https://github.com/rust-lang/crates.io-index" 112 | checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" 113 | 114 | [[package]] 115 | name = "camino" 116 | version = "1.1.7" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "e0ec6b951b160caa93cc0c7b209e5a3bff7aae9062213451ac99493cd844c239" 119 | 120 | [[package]] 121 | name = "cc" 122 | version = "1.1.12" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "68064e60dbf1f17005c2fde4d07c16d8baa506fd7ffed8ccab702d93617975c7" 125 | dependencies = [ 126 | "shlex", 127 | ] 128 | 129 | [[package]] 130 | name = "cfg-if" 131 | version = "1.0.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 134 | 135 | [[package]] 136 | name = "clap" 137 | version = "4.5.15" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "11d8838454fda655dafd3accb2b6e2bea645b9e4078abe84a22ceb947235c5cc" 140 | dependencies = [ 141 | "clap_builder", 142 | "clap_derive", 143 | ] 144 | 145 | [[package]] 146 | name = "clap_builder" 147 | version = "4.5.15" 148 | source = "registry+https://github.com/rust-lang/crates.io-index" 149 | checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" 150 | dependencies = [ 151 | "anstream", 152 | "anstyle", 153 | "clap_lex", 154 | "strsim", 155 | ] 156 | 157 | [[package]] 158 | name = "clap_derive" 159 | version = "4.5.13" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" 162 | dependencies = [ 163 | "heck", 164 | "proc-macro2", 165 | "quote", 166 | "syn", 167 | ] 168 | 169 | [[package]] 170 | name = "clap_lex" 171 | version = "0.7.2" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" 174 | 175 | [[package]] 176 | name = "color-print" 177 | version = "0.3.6" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "1ee543c60ff3888934877a5671f45494dd27ed4ba25c6670b9a7576b7ed7a8c0" 180 | dependencies = [ 181 | "color-print-proc-macro", 182 | ] 183 | 184 | [[package]] 185 | name = "color-print-proc-macro" 186 | version = "0.3.6" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "77ff1a80c5f3cb1ca7c06ffdd71b6a6dd6d8f896c42141fbd43f50ed28dcdb93" 189 | dependencies = [ 190 | "nom", 191 | "proc-macro2", 192 | "quote", 193 | "syn", 194 | ] 195 | 196 | [[package]] 197 | name = "colorchoice" 198 | version = "1.0.2" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" 201 | 202 | [[package]] 203 | name = "core-foundation-sys" 204 | version = "0.8.7" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" 207 | 208 | [[package]] 209 | name = "futures" 210 | version = "0.3.30" 211 | source = "registry+https://github.com/rust-lang/crates.io-index" 212 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 213 | dependencies = [ 214 | "futures-channel", 215 | "futures-core", 216 | "futures-executor", 217 | "futures-io", 218 | "futures-sink", 219 | "futures-task", 220 | "futures-util", 221 | ] 222 | 223 | [[package]] 224 | name = "futures-channel" 225 | version = "0.3.30" 226 | source = "registry+https://github.com/rust-lang/crates.io-index" 227 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 228 | dependencies = [ 229 | "futures-core", 230 | "futures-sink", 231 | ] 232 | 233 | [[package]] 234 | name = "futures-core" 235 | version = "0.3.30" 236 | source = "registry+https://github.com/rust-lang/crates.io-index" 237 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 238 | 239 | [[package]] 240 | name = "futures-executor" 241 | version = "0.3.30" 242 | source = "registry+https://github.com/rust-lang/crates.io-index" 243 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 244 | dependencies = [ 245 | "futures-core", 246 | "futures-task", 247 | "futures-util", 248 | ] 249 | 250 | [[package]] 251 | name = "futures-io" 252 | version = "0.3.30" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 255 | 256 | [[package]] 257 | name = "futures-macro" 258 | version = "0.3.30" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 261 | dependencies = [ 262 | "proc-macro2", 263 | "quote", 264 | "syn", 265 | ] 266 | 267 | [[package]] 268 | name = "futures-sink" 269 | version = "0.3.30" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 272 | 273 | [[package]] 274 | name = "futures-task" 275 | version = "0.3.30" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 278 | 279 | [[package]] 280 | name = "futures-util" 281 | version = "0.3.30" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 284 | dependencies = [ 285 | "futures-channel", 286 | "futures-core", 287 | "futures-io", 288 | "futures-macro", 289 | "futures-sink", 290 | "futures-task", 291 | "memchr", 292 | "pin-project-lite", 293 | "pin-utils", 294 | "slab", 295 | ] 296 | 297 | [[package]] 298 | name = "gimli" 299 | version = "0.29.0" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" 302 | 303 | [[package]] 304 | name = "heck" 305 | version = "0.5.0" 306 | source = "registry+https://github.com/rust-lang/crates.io-index" 307 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 308 | 309 | [[package]] 310 | name = "hermit-abi" 311 | version = "0.3.9" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" 314 | 315 | [[package]] 316 | name = "io-kit-sys" 317 | version = "0.4.1" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" 320 | dependencies = [ 321 | "core-foundation-sys", 322 | "mach2", 323 | ] 324 | 325 | [[package]] 326 | name = "is_terminal_polyfill" 327 | version = "1.70.1" 328 | source = "registry+https://github.com/rust-lang/crates.io-index" 329 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 330 | 331 | [[package]] 332 | name = "lazy_static" 333 | version = "1.5.0" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" 336 | 337 | [[package]] 338 | name = "libc" 339 | version = "0.2.155" 340 | source = "registry+https://github.com/rust-lang/crates.io-index" 341 | checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" 342 | 343 | [[package]] 344 | name = "lock_api" 345 | version = "0.4.12" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" 348 | dependencies = [ 349 | "autocfg", 350 | "scopeguard", 351 | ] 352 | 353 | [[package]] 354 | name = "log" 355 | version = "0.4.22" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" 358 | 359 | [[package]] 360 | name = "mach2" 361 | version = "0.4.2" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" 364 | dependencies = [ 365 | "libc", 366 | ] 367 | 368 | [[package]] 369 | name = "memchr" 370 | version = "2.7.4" 371 | source = "registry+https://github.com/rust-lang/crates.io-index" 372 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 373 | 374 | [[package]] 375 | name = "memoffset" 376 | version = "0.7.1" 377 | source = "registry+https://github.com/rust-lang/crates.io-index" 378 | checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" 379 | dependencies = [ 380 | "autocfg", 381 | ] 382 | 383 | [[package]] 384 | name = "minimal-lexical" 385 | version = "0.2.1" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" 388 | 389 | [[package]] 390 | name = "miniz_oxide" 391 | version = "0.7.4" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" 394 | dependencies = [ 395 | "adler", 396 | ] 397 | 398 | [[package]] 399 | name = "mio" 400 | version = "0.8.11" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" 403 | dependencies = [ 404 | "libc", 405 | "log", 406 | "wasi", 407 | "windows-sys 0.48.0", 408 | ] 409 | 410 | [[package]] 411 | name = "mio" 412 | version = "1.0.2" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" 415 | dependencies = [ 416 | "hermit-abi", 417 | "libc", 418 | "wasi", 419 | "windows-sys 0.52.0", 420 | ] 421 | 422 | [[package]] 423 | name = "mio-serial" 424 | version = "5.0.5" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "20a4c60ca5c9c0e114b3bd66ff4aa5f9b2b175442be51ca6c4365d687a97a2ac" 427 | dependencies = [ 428 | "log", 429 | "mio 0.8.11", 430 | "nix", 431 | "serialport", 432 | "winapi", 433 | ] 434 | 435 | [[package]] 436 | name = "nix" 437 | version = "0.26.4" 438 | source = "registry+https://github.com/rust-lang/crates.io-index" 439 | checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" 440 | dependencies = [ 441 | "bitflags 1.3.2", 442 | "cfg-if", 443 | "libc", 444 | "memoffset", 445 | "pin-utils", 446 | ] 447 | 448 | [[package]] 449 | name = "nom" 450 | version = "7.1.3" 451 | source = "registry+https://github.com/rust-lang/crates.io-index" 452 | checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" 453 | dependencies = [ 454 | "memchr", 455 | "minimal-lexical", 456 | ] 457 | 458 | [[package]] 459 | name = "normalize-line-endings" 460 | version = "0.3.0" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" 463 | 464 | [[package]] 465 | name = "nu-ansi-term" 466 | version = "0.46.0" 467 | source = "registry+https://github.com/rust-lang/crates.io-index" 468 | checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" 469 | dependencies = [ 470 | "overload", 471 | "winapi", 472 | ] 473 | 474 | [[package]] 475 | name = "object" 476 | version = "0.36.3" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" 479 | dependencies = [ 480 | "memchr", 481 | ] 482 | 483 | [[package]] 484 | name = "once_cell" 485 | version = "1.19.0" 486 | source = "registry+https://github.com/rust-lang/crates.io-index" 487 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 488 | 489 | [[package]] 490 | name = "os_pipe" 491 | version = "1.2.1" 492 | source = "registry+https://github.com/rust-lang/crates.io-index" 493 | checksum = "5ffd2b0a5634335b135d5728d84c5e0fd726954b87111f7506a61c502280d982" 494 | dependencies = [ 495 | "libc", 496 | "windows-sys 0.59.0", 497 | ] 498 | 499 | [[package]] 500 | name = "overload" 501 | version = "0.1.1" 502 | source = "registry+https://github.com/rust-lang/crates.io-index" 503 | checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" 504 | 505 | [[package]] 506 | name = "parking_lot" 507 | version = "0.12.3" 508 | source = "registry+https://github.com/rust-lang/crates.io-index" 509 | checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" 510 | dependencies = [ 511 | "lock_api", 512 | "parking_lot_core", 513 | ] 514 | 515 | [[package]] 516 | name = "parking_lot_core" 517 | version = "0.9.10" 518 | source = "registry+https://github.com/rust-lang/crates.io-index" 519 | checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" 520 | dependencies = [ 521 | "cfg-if", 522 | "libc", 523 | "redox_syscall", 524 | "smallvec", 525 | "windows-targets 0.52.6", 526 | ] 527 | 528 | [[package]] 529 | name = "pin-project-lite" 530 | version = "0.2.14" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 533 | 534 | [[package]] 535 | name = "pin-utils" 536 | version = "0.1.0" 537 | source = "registry+https://github.com/rust-lang/crates.io-index" 538 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 539 | 540 | [[package]] 541 | name = "proc-macro2" 542 | version = "1.0.86" 543 | source = "registry+https://github.com/rust-lang/crates.io-index" 544 | checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" 545 | dependencies = [ 546 | "unicode-ident", 547 | ] 548 | 549 | [[package]] 550 | name = "quote" 551 | version = "1.0.36" 552 | source = "registry+https://github.com/rust-lang/crates.io-index" 553 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 554 | dependencies = [ 555 | "proc-macro2", 556 | ] 557 | 558 | [[package]] 559 | name = "redox_syscall" 560 | version = "0.5.3" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" 563 | dependencies = [ 564 | "bitflags 2.6.0", 565 | ] 566 | 567 | [[package]] 568 | name = "rustc-demangle" 569 | version = "0.1.24" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" 572 | 573 | [[package]] 574 | name = "scopeguard" 575 | version = "1.2.0" 576 | source = "registry+https://github.com/rust-lang/crates.io-index" 577 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 578 | 579 | [[package]] 580 | name = "serialport" 581 | version = "4.5.0" 582 | source = "registry+https://github.com/rust-lang/crates.io-index" 583 | checksum = "241ebb629ed9bf598b2b392ba42aa429f9ef2a0099001246a36ac4c084ee183f" 584 | dependencies = [ 585 | "bitflags 2.6.0", 586 | "cfg-if", 587 | "core-foundation-sys", 588 | "io-kit-sys", 589 | "mach2", 590 | "nix", 591 | "scopeguard", 592 | "unescaper", 593 | "winapi", 594 | ] 595 | 596 | [[package]] 597 | name = "sharded-slab" 598 | version = "0.1.7" 599 | source = "registry+https://github.com/rust-lang/crates.io-index" 600 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 601 | dependencies = [ 602 | "lazy_static", 603 | ] 604 | 605 | [[package]] 606 | name = "shlex" 607 | version = "1.3.0" 608 | source = "registry+https://github.com/rust-lang/crates.io-index" 609 | checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" 610 | 611 | [[package]] 612 | name = "signal-hook-registry" 613 | version = "1.4.2" 614 | source = "registry+https://github.com/rust-lang/crates.io-index" 615 | checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" 616 | dependencies = [ 617 | "libc", 618 | ] 619 | 620 | [[package]] 621 | name = "similar" 622 | version = "2.6.0" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "1de1d4f81173b03af4c0cbed3c898f6bff5b870e4a7f5d6f4057d62a7a4b686e" 625 | 626 | [[package]] 627 | name = "slab" 628 | version = "0.4.9" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 631 | dependencies = [ 632 | "autocfg", 633 | ] 634 | 635 | [[package]] 636 | name = "smallvec" 637 | version = "1.13.2" 638 | source = "registry+https://github.com/rust-lang/crates.io-index" 639 | checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" 640 | 641 | [[package]] 642 | name = "snapbox" 643 | version = "0.4.17" 644 | source = "registry+https://github.com/rust-lang/crates.io-index" 645 | checksum = "4b831b6e80fbcd2889efa75b185d24005f85981431495f995292b25836519d84" 646 | dependencies = [ 647 | "anstream", 648 | "anstyle", 649 | "libc", 650 | "normalize-line-endings", 651 | "os_pipe", 652 | "similar", 653 | "snapbox-macros", 654 | "wait-timeout", 655 | "windows-sys 0.52.0", 656 | ] 657 | 658 | [[package]] 659 | name = "snapbox-macros" 660 | version = "0.3.10" 661 | source = "registry+https://github.com/rust-lang/crates.io-index" 662 | checksum = "16569f53ca23a41bb6f62e0a5084aa1661f4814a67fa33696a79073e03a664af" 663 | dependencies = [ 664 | "anstream", 665 | ] 666 | 667 | [[package]] 668 | name = "socket2" 669 | version = "0.5.7" 670 | source = "registry+https://github.com/rust-lang/crates.io-index" 671 | checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" 672 | dependencies = [ 673 | "libc", 674 | "windows-sys 0.52.0", 675 | ] 676 | 677 | [[package]] 678 | name = "strsim" 679 | version = "0.11.1" 680 | source = "registry+https://github.com/rust-lang/crates.io-index" 681 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 682 | 683 | [[package]] 684 | name = "syn" 685 | version = "2.0.74" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" 688 | dependencies = [ 689 | "proc-macro2", 690 | "quote", 691 | "unicode-ident", 692 | ] 693 | 694 | [[package]] 695 | name = "thiserror" 696 | version = "1.0.63" 697 | source = "registry+https://github.com/rust-lang/crates.io-index" 698 | checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" 699 | dependencies = [ 700 | "thiserror-impl", 701 | ] 702 | 703 | [[package]] 704 | name = "thiserror-impl" 705 | version = "1.0.63" 706 | source = "registry+https://github.com/rust-lang/crates.io-index" 707 | checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" 708 | dependencies = [ 709 | "proc-macro2", 710 | "quote", 711 | "syn", 712 | ] 713 | 714 | [[package]] 715 | name = "thread_local" 716 | version = "1.1.8" 717 | source = "registry+https://github.com/rust-lang/crates.io-index" 718 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 719 | dependencies = [ 720 | "cfg-if", 721 | "once_cell", 722 | ] 723 | 724 | [[package]] 725 | name = "tokio" 726 | version = "1.39.2" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" 729 | dependencies = [ 730 | "backtrace", 731 | "bytes", 732 | "libc", 733 | "mio 1.0.2", 734 | "parking_lot", 735 | "pin-project-lite", 736 | "signal-hook-registry", 737 | "socket2", 738 | "tokio-macros", 739 | "windows-sys 0.52.0", 740 | ] 741 | 742 | [[package]] 743 | name = "tokio-macros" 744 | version = "2.4.0" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" 747 | dependencies = [ 748 | "proc-macro2", 749 | "quote", 750 | "syn", 751 | ] 752 | 753 | [[package]] 754 | name = "tokio-serial" 755 | version = "5.4.4" 756 | source = "registry+https://github.com/rust-lang/crates.io-index" 757 | checksum = "aa6e2e4cf0520a99c5f87d5abb24172b5bd220de57c3181baaaa5440540c64aa" 758 | dependencies = [ 759 | "cfg-if", 760 | "futures", 761 | "log", 762 | "mio-serial", 763 | "tokio", 764 | ] 765 | 766 | [[package]] 767 | name = "tokio-stream" 768 | version = "0.1.15" 769 | source = "registry+https://github.com/rust-lang/crates.io-index" 770 | checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" 771 | dependencies = [ 772 | "futures-core", 773 | "pin-project-lite", 774 | "tokio", 775 | ] 776 | 777 | [[package]] 778 | name = "tokio-util" 779 | version = "0.7.11" 780 | source = "registry+https://github.com/rust-lang/crates.io-index" 781 | checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" 782 | dependencies = [ 783 | "bytes", 784 | "futures-core", 785 | "futures-sink", 786 | "pin-project-lite", 787 | "tokio", 788 | ] 789 | 790 | [[package]] 791 | name = "tracing" 792 | version = "0.1.40" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 795 | dependencies = [ 796 | "pin-project-lite", 797 | "tracing-attributes", 798 | "tracing-core", 799 | ] 800 | 801 | [[package]] 802 | name = "tracing-attributes" 803 | version = "0.1.27" 804 | source = "registry+https://github.com/rust-lang/crates.io-index" 805 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 806 | dependencies = [ 807 | "proc-macro2", 808 | "quote", 809 | "syn", 810 | ] 811 | 812 | [[package]] 813 | name = "tracing-core" 814 | version = "0.1.32" 815 | source = "registry+https://github.com/rust-lang/crates.io-index" 816 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 817 | dependencies = [ 818 | "once_cell", 819 | "valuable", 820 | ] 821 | 822 | [[package]] 823 | name = "tracing-log" 824 | version = "0.2.0" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" 827 | dependencies = [ 828 | "log", 829 | "once_cell", 830 | "tracing-core", 831 | ] 832 | 833 | [[package]] 834 | name = "tracing-subscriber" 835 | version = "0.3.18" 836 | source = "registry+https://github.com/rust-lang/crates.io-index" 837 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 838 | dependencies = [ 839 | "nu-ansi-term", 840 | "sharded-slab", 841 | "smallvec", 842 | "thread_local", 843 | "tracing-core", 844 | "tracing-log", 845 | ] 846 | 847 | [[package]] 848 | name = "unescaper" 849 | version = "0.1.5" 850 | source = "registry+https://github.com/rust-lang/crates.io-index" 851 | checksum = "c878a167baa8afd137494101a688ef8c67125089ff2249284bd2b5f9bfedb815" 852 | dependencies = [ 853 | "thiserror", 854 | ] 855 | 856 | [[package]] 857 | name = "unicode-ident" 858 | version = "1.0.12" 859 | source = "registry+https://github.com/rust-lang/crates.io-index" 860 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 861 | 862 | [[package]] 863 | name = "utf8parse" 864 | version = "0.2.2" 865 | source = "registry+https://github.com/rust-lang/crates.io-index" 866 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 867 | 868 | [[package]] 869 | name = "valuable" 870 | version = "0.1.0" 871 | source = "registry+https://github.com/rust-lang/crates.io-index" 872 | checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" 873 | 874 | [[package]] 875 | name = "vsp-router" 876 | version = "1.0.3" 877 | dependencies = [ 878 | "anyhow", 879 | "bytes", 880 | "camino", 881 | "clap", 882 | "color-print", 883 | "futures", 884 | "futures-util", 885 | "snapbox", 886 | "thiserror", 887 | "tokio", 888 | "tokio-serial", 889 | "tokio-stream", 890 | "tokio-util", 891 | "tracing", 892 | "tracing-subscriber", 893 | ] 894 | 895 | [[package]] 896 | name = "wait-timeout" 897 | version = "0.2.0" 898 | source = "registry+https://github.com/rust-lang/crates.io-index" 899 | checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" 900 | dependencies = [ 901 | "libc", 902 | ] 903 | 904 | [[package]] 905 | name = "wasi" 906 | version = "0.11.0+wasi-snapshot-preview1" 907 | source = "registry+https://github.com/rust-lang/crates.io-index" 908 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 909 | 910 | [[package]] 911 | name = "winapi" 912 | version = "0.3.9" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 915 | dependencies = [ 916 | "winapi-i686-pc-windows-gnu", 917 | "winapi-x86_64-pc-windows-gnu", 918 | ] 919 | 920 | [[package]] 921 | name = "winapi-i686-pc-windows-gnu" 922 | version = "0.4.0" 923 | source = "registry+https://github.com/rust-lang/crates.io-index" 924 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 925 | 926 | [[package]] 927 | name = "winapi-x86_64-pc-windows-gnu" 928 | version = "0.4.0" 929 | source = "registry+https://github.com/rust-lang/crates.io-index" 930 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 931 | 932 | [[package]] 933 | name = "windows-sys" 934 | version = "0.48.0" 935 | source = "registry+https://github.com/rust-lang/crates.io-index" 936 | checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 937 | dependencies = [ 938 | "windows-targets 0.48.5", 939 | ] 940 | 941 | [[package]] 942 | name = "windows-sys" 943 | version = "0.52.0" 944 | source = "registry+https://github.com/rust-lang/crates.io-index" 945 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 946 | dependencies = [ 947 | "windows-targets 0.52.6", 948 | ] 949 | 950 | [[package]] 951 | name = "windows-sys" 952 | version = "0.59.0" 953 | source = "registry+https://github.com/rust-lang/crates.io-index" 954 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 955 | dependencies = [ 956 | "windows-targets 0.52.6", 957 | ] 958 | 959 | [[package]] 960 | name = "windows-targets" 961 | version = "0.48.5" 962 | source = "registry+https://github.com/rust-lang/crates.io-index" 963 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 964 | dependencies = [ 965 | "windows_aarch64_gnullvm 0.48.5", 966 | "windows_aarch64_msvc 0.48.5", 967 | "windows_i686_gnu 0.48.5", 968 | "windows_i686_msvc 0.48.5", 969 | "windows_x86_64_gnu 0.48.5", 970 | "windows_x86_64_gnullvm 0.48.5", 971 | "windows_x86_64_msvc 0.48.5", 972 | ] 973 | 974 | [[package]] 975 | name = "windows-targets" 976 | version = "0.52.6" 977 | source = "registry+https://github.com/rust-lang/crates.io-index" 978 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 979 | dependencies = [ 980 | "windows_aarch64_gnullvm 0.52.6", 981 | "windows_aarch64_msvc 0.52.6", 982 | "windows_i686_gnu 0.52.6", 983 | "windows_i686_gnullvm", 984 | "windows_i686_msvc 0.52.6", 985 | "windows_x86_64_gnu 0.52.6", 986 | "windows_x86_64_gnullvm 0.52.6", 987 | "windows_x86_64_msvc 0.52.6", 988 | ] 989 | 990 | [[package]] 991 | name = "windows_aarch64_gnullvm" 992 | version = "0.48.5" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 995 | 996 | [[package]] 997 | name = "windows_aarch64_gnullvm" 998 | version = "0.52.6" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 1001 | 1002 | [[package]] 1003 | name = "windows_aarch64_msvc" 1004 | version = "0.48.5" 1005 | source = "registry+https://github.com/rust-lang/crates.io-index" 1006 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 1007 | 1008 | [[package]] 1009 | name = "windows_aarch64_msvc" 1010 | version = "0.52.6" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 1013 | 1014 | [[package]] 1015 | name = "windows_i686_gnu" 1016 | version = "0.48.5" 1017 | source = "registry+https://github.com/rust-lang/crates.io-index" 1018 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 1019 | 1020 | [[package]] 1021 | name = "windows_i686_gnu" 1022 | version = "0.52.6" 1023 | source = "registry+https://github.com/rust-lang/crates.io-index" 1024 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 1025 | 1026 | [[package]] 1027 | name = "windows_i686_gnullvm" 1028 | version = "0.52.6" 1029 | source = "registry+https://github.com/rust-lang/crates.io-index" 1030 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 1031 | 1032 | [[package]] 1033 | name = "windows_i686_msvc" 1034 | version = "0.48.5" 1035 | source = "registry+https://github.com/rust-lang/crates.io-index" 1036 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 1037 | 1038 | [[package]] 1039 | name = "windows_i686_msvc" 1040 | version = "0.52.6" 1041 | source = "registry+https://github.com/rust-lang/crates.io-index" 1042 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 1043 | 1044 | [[package]] 1045 | name = "windows_x86_64_gnu" 1046 | version = "0.48.5" 1047 | source = "registry+https://github.com/rust-lang/crates.io-index" 1048 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 1049 | 1050 | [[package]] 1051 | name = "windows_x86_64_gnu" 1052 | version = "0.52.6" 1053 | source = "registry+https://github.com/rust-lang/crates.io-index" 1054 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 1055 | 1056 | [[package]] 1057 | name = "windows_x86_64_gnullvm" 1058 | version = "0.48.5" 1059 | source = "registry+https://github.com/rust-lang/crates.io-index" 1060 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 1061 | 1062 | [[package]] 1063 | name = "windows_x86_64_gnullvm" 1064 | version = "0.52.6" 1065 | source = "registry+https://github.com/rust-lang/crates.io-index" 1066 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 1067 | 1068 | [[package]] 1069 | name = "windows_x86_64_msvc" 1070 | version = "0.48.5" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 1073 | 1074 | [[package]] 1075 | name = "windows_x86_64_msvc" 1076 | version = "0.52.6" 1077 | source = "registry+https://github.com/rust-lang/crates.io-index" 1078 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 1079 | --------------------------------------------------------------------------------