Initial commit: RustDesk 1.4.7 macOS desktop port

- Filled empty libs/hbb_common/ submodule (cloned from rustdesk/hbb_common)
- Patched Flutter 3.44 / Dart 3.12 compatibility:
  * flutter/lib/generated_bridge.dart: asTypedList with cast<>, DartPort=Int64
  * flutter/lib/common.dart: DialogTheme->DialogThemeData, TabBarTheme->TabBarThemeData
  * flutter/pubspec.yaml: extended_text 14.0.0->15.0.2, google_fonts override 5.0.0
  * flutter/macos/Runner/Configs/Release.xcconfig: EXCLUDED_ARCHS=x86_64
- Build verified: cargo check + cargo build --features flutter + cargo build --release --features flutter
- Verified flutter build macos --debug and --release both produce working .app
- Verified .dmg installer (27MB arm64) created via hdiutil
- Build deps: Xcode 26.5, CocoaPods 1.16.2, Flutter 3.44, VCPKG arm64-osx
This commit is contained in:
xuwenwei
2026-06-08 21:42:20 +08:00
parent 3d8db09123
commit d3b6026bfd
840 changed files with 291780 additions and 1 deletions
+139
View File
@@ -0,0 +1,139 @@
use std::{
fs::{self},
io::{Cursor, Read},
path::Path,
};
#[cfg(windows)]
const BIN_DATA: &[u8] = include_bytes!("../data.bin");
#[cfg(not(windows))]
const BIN_DATA: &[u8] = &[];
// 4bytes
const LENGTH: usize = 4;
const IDENTIFIER_LENGTH: usize = 8;
const MD5_LENGTH: usize = 32;
const BUF_SIZE: usize = 4096;
pub(crate) struct BinaryData {
pub md5_code: &'static [u8],
// compressed gzip data
pub raw: &'static [u8],
pub path: String,
}
pub(crate) struct BinaryReader {
pub files: Vec<BinaryData>,
pub exe: String,
}
impl Default for BinaryReader {
fn default() -> Self {
let (files, exe) = BinaryReader::read();
Self { files, exe }
}
}
impl BinaryData {
fn decompress(&self) -> Vec<u8> {
let cursor = Cursor::new(self.raw);
let mut decoder = brotli::Decompressor::new(cursor, BUF_SIZE);
let mut buf = Vec::new();
decoder.read_to_end(&mut buf).ok();
buf
}
pub fn write_to_file(&self, prefix: &Path) {
let p = prefix.join(&self.path);
if let Some(parent) = p.parent() {
if !parent.exists() {
let _ = fs::create_dir_all(parent);
}
}
if p.exists() {
// check md5
let f = fs::read(p.clone()).unwrap_or_default();
let digest = format!("{:x}", md5::compute(&f));
let md5_record = String::from_utf8_lossy(self.md5_code);
if digest == md5_record {
// same, skip this file
println!("skip {}", &self.path);
return;
} else {
println!("writing {}", p.display());
println!("{} -> {}", md5_record, digest)
}
}
let _ = fs::write(p, self.decompress());
}
}
impl BinaryReader {
fn read() -> (Vec<BinaryData>, String) {
let mut base: usize = 0;
let mut parsed = vec![];
assert!(BIN_DATA.len() > IDENTIFIER_LENGTH, "bin data invalid!");
let mut iden = String::from_utf8_lossy(&BIN_DATA[base..base + IDENTIFIER_LENGTH]);
if iden != "rustdesk" {
panic!("bin file is not valid!");
}
base += IDENTIFIER_LENGTH;
loop {
iden = String::from_utf8_lossy(&BIN_DATA[base..base + IDENTIFIER_LENGTH]);
if iden == "rustdesk" {
base += IDENTIFIER_LENGTH;
break;
}
// start reading
let mut offset = 0;
let path_length = u32::from_be_bytes([
BIN_DATA[base + offset],
BIN_DATA[base + offset + 1],
BIN_DATA[base + offset + 2],
BIN_DATA[base + offset + 3],
]) as usize;
offset += LENGTH;
let path =
String::from_utf8_lossy(&BIN_DATA[base + offset..base + offset + path_length])
.to_string();
offset += path_length;
// file sz
let file_length = u32::from_be_bytes([
BIN_DATA[base + offset],
BIN_DATA[base + offset + 1],
BIN_DATA[base + offset + 2],
BIN_DATA[base + offset + 3],
]) as usize;
offset += LENGTH;
let raw = &BIN_DATA[base + offset..base + offset + file_length];
offset += file_length;
// md5
let md5 = &BIN_DATA[base + offset..base + offset + MD5_LENGTH];
offset += MD5_LENGTH;
parsed.push(BinaryData {
md5_code: md5,
raw: raw,
path: path,
});
base += offset;
}
// executable
let executable = String::from_utf8_lossy(&BIN_DATA[base..]).to_string();
(parsed, executable)
}
#[cfg(linux)]
pub fn configure_permission(&self, prefix: &Path) {
use std::os::unix::prelude::PermissionsExt;
let exe_path = prefix.join(&self.exe);
if exe_path.exists() {
if let Ok(f) = File::open(exe_path) {
if let Ok(meta) = f.metadata() {
let mut permissions = meta.permissions();
permissions.set_mode(0o755);
f.set_permissions(permissions).ok();
}
}
}
}
}
+248
View File
@@ -0,0 +1,248 @@
#![windows_subsystem = "windows"]
use std::{
path::{Path, PathBuf},
process::{Command, Stdio},
};
use bin_reader::BinaryReader;
pub mod bin_reader;
#[cfg(windows)]
mod ui;
#[cfg(windows)]
const APP_METADATA: &[u8] = include_bytes!("../app_metadata.toml");
#[cfg(not(windows))]
const APP_METADATA: &[u8] = &[];
const APP_METADATA_CONFIG: &str = "meta.toml";
const META_LINE_PREFIX_TIMESTAMP: &str = "timestamp = ";
const APP_PREFIX: &str = "rustdesk";
const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
#[cfg(windows)]
const SET_FOREGROUND_WINDOW_ENV_KEY: &str = "SET_FOREGROUND_WINDOW";
fn is_timestamp_matches(dir: &Path, ts: &mut u64) -> bool {
let Ok(app_metadata) = std::str::from_utf8(APP_METADATA) else {
return true;
};
for line in app_metadata.lines() {
if line.starts_with(META_LINE_PREFIX_TIMESTAMP) {
if let Ok(stored_ts) = line.replace(META_LINE_PREFIX_TIMESTAMP, "").parse::<u64>() {
*ts = stored_ts;
break;
}
}
}
if *ts == 0 {
return true;
}
if let Ok(content) = std::fs::read_to_string(dir.join(APP_METADATA_CONFIG)) {
for line in content.lines() {
if line.starts_with(META_LINE_PREFIX_TIMESTAMP) {
if let Ok(stored_ts) = line.replace(META_LINE_PREFIX_TIMESTAMP, "").parse::<u64>() {
return *ts == stored_ts;
}
}
}
}
false
}
fn write_meta(dir: &Path, ts: u64) {
let meta_file = dir.join(APP_METADATA_CONFIG);
if ts != 0 {
let content = format!("{}{}", META_LINE_PREFIX_TIMESTAMP, ts);
// Ignore is ok here
let _ = std::fs::write(meta_file, content);
}
}
fn setup(
reader: BinaryReader,
dir: Option<PathBuf>,
clear: bool,
_args: &Vec<String>,
_ui: &mut bool,
) -> Option<PathBuf> {
let dir = if let Some(dir) = dir {
dir
} else {
// home dir
if let Some(dir) = dirs::data_local_dir() {
dir.join(APP_PREFIX)
} else {
eprintln!("not found data local dir");
return None;
}
};
let mut ts = 0;
if clear || !is_timestamp_matches(&dir, &mut ts) {
#[cfg(windows)]
if _args.is_empty() {
*_ui = true;
ui::setup();
}
std::fs::remove_dir_all(&dir).ok();
}
for file in reader.files.iter() {
file.write_to_file(&dir);
}
write_meta(&dir, ts);
#[cfg(windows)]
win::copy_runtime_broker(&dir);
#[cfg(linux)]
reader.configure_permission(&dir);
Some(dir.join(&reader.exe))
}
fn use_null_stdio() -> bool {
#[cfg(windows)]
{
// When running in CMD on Windows 7, using Stdio::inherit() with spawn returns an "invalid handle" error.
// Since using Stdio::null() didnt cause any issues, and determining whether the program is launched from CMD or by double-clicking would require calling more APIs during startup, we also use Stdio::null() when launched by double-clicking on Windows 7.
let is_windows_7 = is_windows_7();
println!("is windows7: {}", is_windows_7);
return is_windows_7;
}
#[cfg(not(windows))]
false
}
#[cfg(windows)]
fn is_windows_7() -> bool {
use windows::Wdk::System::SystemServices::RtlGetVersion;
use windows::Win32::System::SystemInformation::OSVERSIONINFOW;
unsafe {
let mut version_info = OSVERSIONINFOW::default();
version_info.dwOSVersionInfoSize = std::mem::size_of::<OSVERSIONINFOW>() as u32;
if RtlGetVersion(&mut version_info).is_ok() {
// Windows 7 is version 6.1
println!(
"Windows version: {}.{}",
version_info.dwMajorVersion, version_info.dwMinorVersion
);
return version_info.dwMajorVersion == 6 && version_info.dwMinorVersion == 1;
}
}
false
}
fn execute(path: PathBuf, args: Vec<String>, _ui: bool) {
println!("executing {}", path.display());
// setup env
let exe = std::env::current_exe().unwrap_or_default();
let exe_name = exe.file_name().unwrap_or_default();
// run executable
let mut cmd = Command::new(path);
cmd.args(args);
#[cfg(windows)]
{
use std::os::windows::process::CommandExt;
cmd.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW);
if _ui {
cmd.env(SET_FOREGROUND_WINDOW_ENV_KEY, "1");
}
}
cmd.env(APPNAME_RUNTIME_ENV_KEY, exe_name);
if use_null_stdio() {
cmd.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null());
} else {
cmd.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit());
}
let _child = cmd.spawn();
#[cfg(windows)]
if _ui {
match _child {
Ok(child) => unsafe {
winapi::um::winuser::AllowSetForegroundWindow(child.id() as u32);
},
Err(e) => {
eprintln!("{:?}", e);
}
}
}
}
fn main() {
let mut args = Vec::new();
let mut arg_exe = Default::default();
let mut i = 0;
for arg in std::env::args() {
if i == 0 {
arg_exe = arg.clone();
} else {
args.push(arg);
}
i += 1;
}
let click_setup = args.is_empty() && arg_exe.to_lowercase().ends_with("install.exe");
#[cfg(windows)]
let quick_support = args.is_empty() && win::is_quick_support_exe(&arg_exe);
#[cfg(not(windows))]
let quick_support = false;
let mut ui = false;
let reader = BinaryReader::default();
if let Some(exe) = setup(
reader,
None,
click_setup || args.contains(&"--silent-install".to_owned()),
&args,
&mut ui,
) {
if click_setup {
args = vec!["--install".to_owned()];
} else if quick_support {
args = vec!["--quick_support".to_owned()];
}
execute(exe, args, ui);
}
}
#[cfg(windows)]
mod win {
use std::{fs, os::windows::process::CommandExt, path::Path, process::Command};
// Used for privacy mode(magnifier impl).
pub const RUNTIME_BROKER_EXE: &'static str = "C:\\Windows\\System32\\RuntimeBroker.exe";
pub const WIN_TOPMOST_INJECTED_PROCESS_EXE: &'static str = "RuntimeBroker_rustdesk.exe";
pub(super) fn copy_runtime_broker(dir: &Path) {
let src = RUNTIME_BROKER_EXE;
let tgt = WIN_TOPMOST_INJECTED_PROCESS_EXE;
let target_file = dir.join(tgt);
if target_file.exists() {
if let (Ok(src_file), Ok(tgt_file)) = (fs::read(src), fs::read(&target_file)) {
let src_md5 = format!("{:x}", md5::compute(&src_file));
let tgt_md5 = format!("{:x}", md5::compute(&tgt_file));
if src_md5 == tgt_md5 {
return;
}
}
}
let _allow_err = Command::new("taskkill")
.args(&["/F", "/IM", "RuntimeBroker_rustdesk.exe"])
.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW)
.output();
let _allow_err = std::fs::copy(src, &format!("{}\\{}", dir.to_string_lossy(), tgt));
}
/// Check if the executable is a Quick Support version.
/// Note: This function must be kept in sync with `src/core_main.rs`.
#[inline]
pub(super) fn is_quick_support_exe(exe: &str) -> bool {
let exe = exe.to_lowercase();
exe.contains("-qs-") || exe.contains("-qs.exe") || exe.contains("_qs.exe")
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

+232
View File
@@ -0,0 +1,232 @@
use native_windows_gui as nwg;
use nwg::NativeUi;
use std::cell::RefCell;
const GIF_DATA: &[u8] = include_bytes!("./res/spin.gif");
const LABEL_DATA: &[u8] = include_bytes!("./res/label.png");
const GIF_SIZE: i32 = 32;
const BG_COLOR: [u8; 3] = [90, 90, 120];
const BORDER_COLOR: [u8; 3] = [40, 40, 40];
const GIF_DELAY: u64 = 30;
#[derive(Default)]
pub struct BasicApp {
window: nwg::Window,
border_image: nwg::ImageFrame,
bg_image: nwg::ImageFrame,
gif_image: nwg::ImageFrame,
label_image: nwg::ImageFrame,
border_layout: nwg::GridLayout,
bg_layout: nwg::GridLayout,
inner_layout: nwg::GridLayout,
timer: nwg::AnimationTimer,
decoder: nwg::ImageDecoder,
gif_index: RefCell<usize>,
gif_images: RefCell<Vec<nwg::Bitmap>>,
}
impl BasicApp {
fn exit(&self) {
self.timer.stop();
nwg::stop_thread_dispatch();
}
fn load_gif(&self) -> Result<(), nwg::NwgError> {
let image_source = self.decoder.from_stream(GIF_DATA)?;
for frame_index in 0..image_source.frame_count() {
let image_data = image_source.frame(frame_index)?;
let image_data = self
.decoder
.resize_image(&image_data, [GIF_SIZE as u32, GIF_SIZE as u32])?;
let bmp = image_data.as_bitmap()?;
self.gif_images.borrow_mut().push(bmp);
}
Ok(())
}
fn update_gif(&self) -> Result<(), nwg::NwgError> {
let images = self.gif_images.borrow();
if images.len() == 0 {
return Err(nwg::NwgError::ImageDecoderError(
-1,
"no gif images".to_string(),
));
}
let image_index = *self.gif_index.borrow() % images.len();
self.gif_image.set_bitmap(Some(&images[image_index]));
*self.gif_index.borrow_mut() = (image_index + 1) % images.len();
Ok(())
}
fn start_timer(&self) {
self.timer.start();
}
}
mod basic_app_ui {
use super::*;
use native_windows_gui::{self as nwg, Bitmap};
use nwg::{Event, GridLayoutItem};
use std::cell::RefCell;
use std::ops::Deref;
use std::rc::Rc;
pub struct BasicAppUi {
inner: Rc<BasicApp>,
default_handler: RefCell<Vec<nwg::EventHandler>>,
}
impl nwg::NativeUi<BasicAppUi> for BasicApp {
fn build_ui(mut data: BasicApp) -> Result<BasicAppUi, nwg::NwgError> {
data.decoder = nwg::ImageDecoder::new()?;
let col_cnt: i32 = 7;
let row_cnt: i32 = 3;
let border_width: i32 = 1;
let window_size = (
GIF_SIZE * col_cnt + 2 * border_width,
GIF_SIZE * row_cnt + 2 * border_width,
);
// Controls
nwg::Window::builder()
.flags(nwg::WindowFlags::POPUP | nwg::WindowFlags::VISIBLE)
.size(window_size)
.center(true)
.build(&mut data.window)?;
nwg::ImageFrame::builder()
.parent(&data.window)
.size(window_size)
.background_color(Some(BORDER_COLOR))
.build(&mut data.border_image)?;
nwg::ImageFrame::builder()
.parent(&data.border_image)
.size((row_cnt * GIF_SIZE, col_cnt * GIF_SIZE))
.background_color(Some(BG_COLOR))
.build(&mut data.bg_image)?;
nwg::ImageFrame::builder()
.parent(&data.bg_image)
.size((GIF_SIZE, GIF_SIZE))
.background_color(Some(BG_COLOR))
.build(&mut data.gif_image)?;
nwg::ImageFrame::builder()
.parent(&data.bg_image)
.background_color(Some(BG_COLOR))
.bitmap(Some(&Bitmap::from_bin(LABEL_DATA)?))
.build(&mut data.label_image)?;
nwg::AnimationTimer::builder()
.parent(&data.window)
.interval(std::time::Duration::from_millis(GIF_DELAY))
.build(&mut data.timer)?;
// Wrap-up
let ui = BasicAppUi {
inner: Rc::new(data),
default_handler: Default::default(),
};
// Layouts
nwg::GridLayout::builder()
.parent(&ui.window)
.spacing(0)
.margin([0, 0, 0, 0])
.max_column(Some(1))
.max_row(Some(1))
.child_item(GridLayoutItem::new(&ui.border_image, 0, 0, 1, 1))
.build(&ui.border_layout)?;
nwg::GridLayout::builder()
.parent(&ui.border_image)
.spacing(0)
.margin([
border_width as _,
border_width as _,
border_width as _,
border_width as _,
])
.max_column(Some(1))
.max_row(Some(1))
.child_item(GridLayoutItem::new(&ui.bg_image, 0, 0, 1, 1))
.build(&ui.bg_layout)?;
nwg::GridLayout::builder()
.parent(&ui.bg_image)
.spacing(0)
.margin([0, 0, 0, 0])
.max_column(Some(col_cnt as _))
.max_row(Some(row_cnt as _))
.child_item(GridLayoutItem::new(&ui.gif_image, 2, 1, 1, 1))
.child_item(GridLayoutItem::new(&ui.label_image, 3, 1, 3, 1))
.build(&ui.inner_layout)?;
// Events
let evt_ui = Rc::downgrade(&ui.inner);
let handle_events = move |evt, _evt_data, _handle| {
if let Some(evt_ui) = evt_ui.upgrade().as_mut() {
match evt {
Event::OnWindowClose => {
evt_ui.exit();
}
Event::OnTimerTick => {
if let Err(e) = evt_ui.update_gif() {
eprintln!("{:?}", e);
}
}
_ => {}
}
}
};
ui.default_handler
.borrow_mut()
.push(nwg::full_bind_event_handler(
&ui.window.handle,
handle_events,
));
return Ok(ui);
}
}
impl Drop for BasicAppUi {
/// To make sure that everything is freed without issues, the default handler must be unbound.
fn drop(&mut self) {
let mut handlers = self.default_handler.borrow_mut();
for handler in handlers.drain(0..) {
nwg::unbind_event_handler(&handler);
}
}
}
impl Deref for BasicAppUi {
type Target = BasicApp;
fn deref(&self) -> &BasicApp {
&self.inner
}
}
}
fn ui() -> Result<(), nwg::NwgError> {
nwg::init()?;
let app = BasicApp::build_ui(Default::default())?;
app.load_gif()?;
app.start_timer();
nwg::dispatch_thread_events();
Ok(())
}
pub fn setup() {
std::thread::spawn(move || {
if let Err(e) = ui() {
eprintln!("{:?}", e);
}
});
}