From 0037701828a276df7589de65654ddf4c86530405 Mon Sep 17 00:00:00 2001 From: GroM Date: Thu, 6 Nov 2025 18:01:45 +0100 Subject: [PATCH 1/5] Bump bollard + use proper Ledger Docker container --- sim/Cargo.toml | 2 +- sim/src/drivers/docker.rs | 96 +++++++++++++++++++++++---------------- sim/src/lib.rs | 20 ++++++-- 3 files changed, 75 insertions(+), 43 deletions(-) diff --git a/sim/Cargo.toml b/sim/Cargo.toml index c9afbd3..606ece7 100644 --- a/sim/Cargo.toml +++ b/sim/Cargo.toml @@ -16,7 +16,7 @@ tokio = { version = "1.20", features = [ "full" ] } lazy_static = "1.4" tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } -bollard = "0.18" +bollard = "0.19.4" async-trait = "0.1" futures = "0.3" tar = "0.4" diff --git a/sim/src/drivers/docker.rs b/sim/src/drivers/docker.rs index 096796f..bbc1aab 100644 --- a/sim/src/drivers/docker.rs +++ b/sim/src/drivers/docker.rs @@ -10,13 +10,20 @@ use std::{ use async_trait::async_trait; use bollard::{ - container::{ - Config, CreateContainerOptions, LogsOptions, RemoveContainerOptions, StartContainerOptions, - StopContainerOptions, UploadToContainerOptions, - }, - service::{ContainerStateStatusEnum, HostConfig, PortBinding}, - Docker, + Docker, models::{ContainerStateStatusEnum, PortBinding} }; +use bollard::query_parameters::CreateContainerOptionsBuilder; +use bollard::models::ContainerCreateBody; +use bollard::query_parameters::{ + StartContainerOptionsBuilder, + RemoveContainerOptionsBuilder, + UploadToContainerOptionsBuilder, + LogsOptionsBuilder, + InspectContainerOptionsBuilder, + StopContainerOptionsBuilder, +}; +use bollard::models::HostConfig; +use bollard::body_full; use bytes::{BufMut, BytesMut}; use futures::StreamExt; use tokio::sync::oneshot::{channel, Sender}; @@ -49,7 +56,7 @@ impl DockerDriver { } } -const DEFAULT_IMAGE: &str = "ghcr.io/ledgerhq/speculos"; +const DEFAULT_IMAGE: &str = "ghcr.io/ledgerhq/ledger-app-builder/ledger-app-dev-tools:latest"; /// [Driver] implementation for [DockerDriver] #[async_trait] @@ -59,10 +66,10 @@ impl Driver for DockerDriver { async fn run(&self, app: &str, opts: Options) -> anyhow::Result { // Set container name let name = format!("speculos-{}", opts.http_port); - let create_options = Some(CreateContainerOptions { - name: &name, - platform: None, - }); + + let create_options = CreateContainerOptionsBuilder::default() + .name(&name) + .build(); // Setup ports let mut ports = vec![opts.http_port]; @@ -82,14 +89,15 @@ impl Driver for DockerDriver { let app_file = app_path.file_name().and_then(|n| n.to_str()).unwrap(); // Setup speculos command - let mut cmd = vec![]; + + let mut cmd = vec!["speculos".to_string()]; cmd.append(&mut opts.args()); cmd.push(format!("/app/{app_file}")); debug!("command: {}", cmd.join(" ")); // Setup container - let create_config = Config { + let create_config = ContainerCreateBody { image: Some(DEFAULT_IMAGE.to_string()), cmd: Some(cmd), attach_stdout: Some(true), @@ -99,21 +107,26 @@ impl Driver for DockerDriver { exposed_ports.clone().map(|p| (p.0, p.2)), )), host_config: Some(HostConfig { + binds: Some(vec![ + String::from("/tmp/.X11-unix:/tmp/.X11-unix"), + ]), port_bindings: Some(HashMap::from_iter(exposed_ports.map(|p| (p.0, Some(p.1))))), ..Default::default() }), + env: Some(vec![ + String::from("DISPLAY=host.docker.internal:0"), + ]), ..Default::default() }; // Remove existing container if there is one + let remove_options = RemoveContainerOptionsBuilder::new() + .build(); let _ = self .d .remove_container( &name, - Some(RemoveContainerOptions { - force: true, - ..Default::default() - }), + Some(remove_options) ) .await; @@ -121,7 +134,7 @@ impl Driver for DockerDriver { debug!("Creating container {}", name); let _create_info = self .d - .create_container(create_options, create_config) + .create_container(Some(create_options), create_config) .await?; // Generate application archive @@ -134,19 +147,20 @@ impl Driver for DockerDriver { drop(tar); // Write app archive to container - let upload_options = UploadToContainerOptions { - path: "/", - ..Default::default() - }; + let upload_options = UploadToContainerOptionsBuilder::new() + .path("/") + .build(); + self.d - .upload_to_container(&name, Some(upload_options), buff.to_vec().into()) + .upload_to_container(&name, Some(upload_options), body_full(buff.to_vec().into())) .await?; // Start container debug!("Starting container {}", name); + let start_options = StartContainerOptionsBuilder::new().build(); let _start_info = self .d - .start_container(&name, None::>) + .start_container(&name, Some(start_options)) .await?; debug!("Container started"); @@ -154,14 +168,15 @@ impl Driver for DockerDriver { let (exit_tx, mut exit_rx) = channel(); // Setup log streaming task - let mut logs = self.d.logs::( + let log_options = LogsOptionsBuilder::new() + .stderr(true) + .stdout(true) + .follow(true) + .build(); + + let mut logs = self.d.logs( &name, - Some(LogsOptions { - stderr: true, - stdout: true, - follow: true, - ..Default::default() - }), + Some(log_options), ); tokio::task::spawn(async move { @@ -204,8 +219,11 @@ impl Driver for DockerDriver { // Poll container info periodically loop { + + let inspect_options = InspectContainerOptionsBuilder::new().build(); + // Fetch container info - let info = self.d.inspect_container(&handle.name, None).await?; + let info = self.d.inspect_container(&handle.name, Some(inspect_options)).await?; debug!("info: {:?}", info); @@ -229,16 +247,18 @@ impl Driver for DockerDriver { let _ = handle.exit_tx.send(()); // Send container stop signal - let options = Some(StopContainerOptions { t: 0 }); + let stop_options = StopContainerOptionsBuilder::new() + .t(0) + .build(); + let options = Some(stop_options); let _ = self.d.stop_container(&handle.name, options).await; // Remove container debug!("Removing container"); - let options = Some(RemoveContainerOptions { - force: true, - ..Default::default() - }); - self.d.remove_container(&handle.name, options).await?; + let remove_options = RemoveContainerOptionsBuilder::new() + .force(true) + .build(); + self.d.remove_container(&handle.name, Some(remove_options)).await?; debug!("Container removed"); diff --git a/sim/src/lib.rs b/sim/src/lib.rs index 4d5321d..d1db53c 100644 --- a/sim/src/lib.rs +++ b/sim/src/lib.rs @@ -74,6 +74,9 @@ pub enum Model { NanoSP, /// Nano X NanoX, + Stax, + Flex, + ApexP } impl Model { @@ -83,6 +86,9 @@ impl Model { Model::NanoS => "nanos", Model::NanoSP => "nanosplus", Model::NanoX => "nanox", + Model::Stax => "stax", + Model::Flex => "flex", + Model::ApexP => "apex_p", } } } @@ -107,8 +113,8 @@ pub struct Options { pub model: Model, /// Display mode - #[clap(long, value_enum, default_value_t = Options::default().display)] - pub display: Display, + #[clap(long, value_enum)] + pub display: Option, /// SDK version override (defaults based on --model) #[clap(long)] @@ -147,7 +153,7 @@ impl Default for Options { fn default() -> Self { Self { model: Model::NanoSP, - display: Display::Headless, + display: None, sdk: None, api_level: None, seed: None, @@ -166,10 +172,13 @@ impl Options { // Basic args let mut args = vec![ format!("--model={}", self.model), - format!("--display={}", self.display), format!("--api-port={}", self.http_port), ]; + if let Some(display) = &self.display { + args.push(format!("--display={display}")); + } + if let Some(seed) = &self.seed { args.push(format!("--seed={seed}")); } @@ -222,6 +231,9 @@ mod tests { (Model::NanoSP, "nanosp", "nanosp"), (Model::NanoSP, "nanosp", "nanosplus"), (Model::NanoX, "nanox", "nanox"), + (Model::Stax, "stax", "stax"), + (Model::Flex, "flex", "flex"), + (Model::ApexP, "apex_p", "apex_p"), ]; for (model, enc, dec) in t { From 024f22f4af62eec1f57f4238fb914bbb2c843f1e Mon Sep 17 00:00:00 2001 From: GroM Date: Fri, 7 Nov 2025 16:17:53 +0100 Subject: [PATCH 2/5] Fix lint + api port --- sim/src/drivers/docker.rs | 80 ++++++++++++++------------------------- sim/src/lib.rs | 5 ++- 2 files changed, 31 insertions(+), 54 deletions(-) diff --git a/sim/src/drivers/docker.rs b/sim/src/drivers/docker.rs index bbc1aab..409d9f5 100644 --- a/sim/src/drivers/docker.rs +++ b/sim/src/drivers/docker.rs @@ -9,21 +9,18 @@ use std::{ }; use async_trait::async_trait; -use bollard::{ - Docker, models::{ContainerStateStatusEnum, PortBinding} -}; -use bollard::query_parameters::CreateContainerOptionsBuilder; +use bollard::body_full; use bollard::models::ContainerCreateBody; +use bollard::models::HostConfig; +use bollard::query_parameters::CreateContainerOptionsBuilder; use bollard::query_parameters::{ - StartContainerOptionsBuilder, - RemoveContainerOptionsBuilder, - UploadToContainerOptionsBuilder, - LogsOptionsBuilder, - InspectContainerOptionsBuilder, - StopContainerOptionsBuilder, + InspectContainerOptionsBuilder, LogsOptionsBuilder, RemoveContainerOptionsBuilder, + StartContainerOptionsBuilder, StopContainerOptionsBuilder, UploadToContainerOptionsBuilder, +}; +use bollard::{ + models::{ContainerStateStatusEnum, PortBinding}, + Docker, }; -use bollard::models::HostConfig; -use bollard::body_full; use bytes::{BufMut, BytesMut}; use futures::StreamExt; use tokio::sync::oneshot::{channel, Sender}; @@ -65,11 +62,9 @@ impl Driver for DockerDriver { async fn run(&self, app: &str, opts: Options) -> anyhow::Result { // Set container name - let name = format!("speculos-{}", opts.http_port); + let name = format!("speculos-{}", opts.model.to_string().to_lowercase()); - let create_options = CreateContainerOptionsBuilder::default() - .name(&name) - .build(); + let create_options = CreateContainerOptionsBuilder::default().name(&name).build(); // Setup ports let mut ports = vec![opts.http_port]; @@ -89,7 +84,7 @@ impl Driver for DockerDriver { let app_file = app_path.file_name().and_then(|n| n.to_str()).unwrap(); // Setup speculos command - + let mut cmd = vec!["speculos".to_string()]; cmd.append(&mut opts.args()); cmd.push(format!("/app/{app_file}")); @@ -107,28 +102,17 @@ impl Driver for DockerDriver { exposed_ports.clone().map(|p| (p.0, p.2)), )), host_config: Some(HostConfig { - binds: Some(vec![ - String::from("/tmp/.X11-unix:/tmp/.X11-unix"), - ]), + binds: Some(vec![String::from("/tmp/.X11-unix:/tmp/.X11-unix")]), port_bindings: Some(HashMap::from_iter(exposed_ports.map(|p| (p.0, Some(p.1))))), ..Default::default() }), - env: Some(vec![ - String::from("DISPLAY=host.docker.internal:0"), - ]), + env: Some(vec![String::from("DISPLAY=host.docker.internal:0")]), ..Default::default() }; // Remove existing container if there is one - let remove_options = RemoveContainerOptionsBuilder::new() - .build(); - let _ = self - .d - .remove_container( - &name, - Some(remove_options) - ) - .await; + let remove_options = RemoveContainerOptionsBuilder::new().build(); + let _ = self.d.remove_container(&name, Some(remove_options)).await; // Create container debug!("Creating container {}", name); @@ -147,9 +131,7 @@ impl Driver for DockerDriver { drop(tar); // Write app archive to container - let upload_options = UploadToContainerOptionsBuilder::new() - .path("/") - .build(); + let upload_options = UploadToContainerOptionsBuilder::new().path("/").build(); self.d .upload_to_container(&name, Some(upload_options), body_full(buff.to_vec().into())) @@ -158,10 +140,7 @@ impl Driver for DockerDriver { // Start container debug!("Starting container {}", name); let start_options = StartContainerOptionsBuilder::new().build(); - let _start_info = self - .d - .start_container(&name, Some(start_options)) - .await?; + let _start_info = self.d.start_container(&name, Some(start_options)).await?; debug!("Container started"); @@ -174,10 +153,7 @@ impl Driver for DockerDriver { .follow(true) .build(); - let mut logs = self.d.logs( - &name, - Some(log_options), - ); + let mut logs = self.d.logs(&name, Some(log_options)); tokio::task::spawn(async move { debug!("start log task"); @@ -219,11 +195,13 @@ impl Driver for DockerDriver { // Poll container info periodically loop { - let inspect_options = InspectContainerOptionsBuilder::new().build(); // Fetch container info - let info = self.d.inspect_container(&handle.name, Some(inspect_options)).await?; + let info = self + .d + .inspect_container(&handle.name, Some(inspect_options)) + .await?; debug!("info: {:?}", info); @@ -247,18 +225,16 @@ impl Driver for DockerDriver { let _ = handle.exit_tx.send(()); // Send container stop signal - let stop_options = StopContainerOptionsBuilder::new() - .t(0) - .build(); + let stop_options = StopContainerOptionsBuilder::new().t(0).build(); let options = Some(stop_options); let _ = self.d.stop_container(&handle.name, options).await; // Remove container debug!("Removing container"); - let remove_options = RemoveContainerOptionsBuilder::new() - .force(true) - .build(); - self.d.remove_container(&handle.name, Some(remove_options)).await?; + let remove_options = RemoveContainerOptionsBuilder::new().force(true).build(); + self.d + .remove_container(&handle.name, Some(remove_options)) + .await?; debug!("Container removed"); diff --git a/sim/src/lib.rs b/sim/src/lib.rs index d1db53c..34bc5b7 100644 --- a/sim/src/lib.rs +++ b/sim/src/lib.rs @@ -76,7 +76,8 @@ pub enum Model { NanoX, Stax, Flex, - ApexP + #[strum(serialize = "apex_p")] + ApexP, } impl Model { @@ -157,7 +158,7 @@ impl Default for Options { sdk: None, api_level: None, seed: None, - http_port: 5000, + http_port: 5001, apdu_port: None, debug: false, root: None, From 6c0968eaec82a52361e66f55ddff7c04a6d817ed Mon Sep 17 00:00:00 2001 From: GroM Date: Fri, 7 Nov 2025 16:54:37 +0100 Subject: [PATCH 3/5] Manage display for Stax/Flex/NanoGen5 according to OS --- sim/src/drivers/docker.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sim/src/drivers/docker.rs b/sim/src/drivers/docker.rs index 409d9f5..9594b30 100644 --- a/sim/src/drivers/docker.rs +++ b/sim/src/drivers/docker.rs @@ -102,11 +102,15 @@ impl Driver for DockerDriver { exposed_ports.clone().map(|p| (p.0, p.2)), )), host_config: Some(HostConfig { + #[cfg(any(target_os = "linux", target_os = "macos"))] binds: Some(vec![String::from("/tmp/.X11-unix:/tmp/.X11-unix")]), port_bindings: Some(HashMap::from_iter(exposed_ports.map(|p| (p.0, Some(p.1))))), ..Default::default() }), + #[cfg(any(target_os = "windows", target_os = "macos"))] env: Some(vec![String::from("DISPLAY=host.docker.internal:0")]), + #[cfg(target_os = "linux")] + env: Some(vec![String::from("DISPLAY=$DISPLAY")]), ..Default::default() }; @@ -203,8 +207,6 @@ impl Driver for DockerDriver { .inspect_container(&handle.name, Some(inspect_options)) .await?; - debug!("info: {:?}", info); - // Return when container exits match info.state.and_then(|s| s.status) { Some(CREATED) | Some(RUNNING) => (), From 2d86ec640e5843e3d6ba6ae2f781657730c4e3c8 Mon Sep 17 00:00:00 2001 From: GroM Date: Fri, 7 Nov 2025 16:57:55 +0100 Subject: [PATCH 4/5] Update last Ledger product name --- sim/src/lib.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/sim/src/lib.rs b/sim/src/lib.rs index 34bc5b7..8388990 100644 --- a/sim/src/lib.rs +++ b/sim/src/lib.rs @@ -76,8 +76,7 @@ pub enum Model { NanoX, Stax, Flex, - #[strum(serialize = "apex_p")] - ApexP, + NanoGen5, } impl Model { @@ -89,7 +88,7 @@ impl Model { Model::NanoX => "nanox", Model::Stax => "stax", Model::Flex => "flex", - Model::ApexP => "apex_p", + Model::NanoGen5 => "nanogen5", } } } @@ -234,7 +233,7 @@ mod tests { (Model::NanoX, "nanox", "nanox"), (Model::Stax, "stax", "stax"), (Model::Flex, "flex", "flex"), - (Model::ApexP, "apex_p", "apex_p"), + (Model::NanoGen5, "nanogen5", "nanogen5"), ]; for (model, enc, dec) in t { From 02448a61639a2620c4fc6adebc2ffec6e940b242 Mon Sep 17 00:00:00 2001 From: GroM Date: Mon, 10 Nov 2025 10:31:42 +0100 Subject: [PATCH 5/5] Updates after Copilot PR review --- sim/src/drivers/docker.rs | 24 +++++++++++++++++------- sim/src/lib.rs | 3 +++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/sim/src/drivers/docker.rs b/sim/src/drivers/docker.rs index 9594b30..7979617 100644 --- a/sim/src/drivers/docker.rs +++ b/sim/src/drivers/docker.rs @@ -62,7 +62,11 @@ impl Driver for DockerDriver { async fn run(&self, app: &str, opts: Options) -> anyhow::Result { // Set container name - let name = format!("speculos-{}", opts.model.to_string().to_lowercase()); + let name = format!( + "speculos-{}-{}", + opts.model.to_string().to_lowercase(), + opts.http_port + ); let create_options = CreateContainerOptionsBuilder::default().name(&name).build(); @@ -102,20 +106,26 @@ impl Driver for DockerDriver { exposed_ports.clone().map(|p| (p.0, p.2)), )), host_config: Some(HostConfig { - #[cfg(any(target_os = "linux", target_os = "macos"))] + #[cfg(target_os = "linux")] binds: Some(vec![String::from("/tmp/.X11-unix:/tmp/.X11-unix")]), port_bindings: Some(HashMap::from_iter(exposed_ports.map(|p| (p.0, Some(p.1))))), ..Default::default() }), - #[cfg(any(target_os = "windows", target_os = "macos"))] - env: Some(vec![String::from("DISPLAY=host.docker.internal:0")]), - #[cfg(target_os = "linux")] - env: Some(vec![String::from("DISPLAY=$DISPLAY")]), + + env: { + #[cfg(any(target_os = "windows", target_os = "macos"))] + let display = Some(vec![String::from("DISPLAY=host.docker.internal:0")]); + #[cfg(target_os = "linux")] + let display = std::env::var("DISPLAY") + .ok() + .map(|d| vec![format!("DISPLAY={}", d)]); + display + }, ..Default::default() }; // Remove existing container if there is one - let remove_options = RemoveContainerOptionsBuilder::new().build(); + let remove_options = RemoveContainerOptionsBuilder::new().force(true).build(); let _ = self.d.remove_container(&name, Some(remove_options)).await; // Create container diff --git a/sim/src/lib.rs b/sim/src/lib.rs index 8388990..33487b7 100644 --- a/sim/src/lib.rs +++ b/sim/src/lib.rs @@ -74,8 +74,11 @@ pub enum Model { NanoSP, /// Nano X NanoX, + /// Stax Stax, + /// Flex Flex, + /// Nano Gen 5 NanoGen5, }