From 8c10675d8a2638a6ea2d497ab7392733b02dd255 Mon Sep 17 00:00:00 2001
From: Kingtous <kingtous@qq.com>
Date: Wed, 21 Sep 2022 11:28:28 +0800
Subject: [PATCH] feat: windows portable build script

---
 Cargo.toml                      |   2 +-
 build.py                        |  27 ++-
 libs/portable/.gitignore        |   3 +
 libs/portable/Cargo.lock        | 285 ++++++++++++++++++++++++++++++++
 libs/portable/Cargo.toml        |  16 ++
 libs/portable/build.rs          |   5 +
 libs/portable/generate.py       |  88 ++++++++++
 libs/portable/icon.rc           |   1 +
 libs/portable/requirements.txt  |   1 +
 libs/portable/src/bin_reader.rs | 134 +++++++++++++++
 libs/portable/src/main.rs       |  51 ++++++
 11 files changed, 611 insertions(+), 2 deletions(-)
 create mode 100644 libs/portable/.gitignore
 create mode 100644 libs/portable/Cargo.lock
 create mode 100644 libs/portable/Cargo.toml
 create mode 100644 libs/portable/build.rs
 create mode 100644 libs/portable/generate.py
 create mode 100644 libs/portable/icon.rc
 create mode 100644 libs/portable/requirements.txt
 create mode 100644 libs/portable/src/bin_reader.rs
 create mode 100644 libs/portable/src/main.rs

diff --git a/Cargo.toml b/Cargo.toml
index 8670ade40..062e32abb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -119,7 +119,7 @@ jni = "0.19"
 flutter_rust_bridge = { git = "https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge" }
 
 [workspace]
-members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/simple_rc"]
+members = ["libs/scrap", "libs/hbb_common", "libs/enigo", "libs/clipboard", "libs/virtual_display", "libs/simple_rc", "libs/portable"]
 
 [package.metadata.winres]
 LegalCopyright = "Copyright © 2022 Purslane, Inc."
diff --git a/build.py b/build.py
index 52ccbe41e..2c08e5af0 100755
--- a/build.py
+++ b/build.py
@@ -73,6 +73,11 @@ def make_parser():
         action='store_true',
         help='Enable feature hwcodec'
     )
+    parser.add_argument(
+        '--portable',
+        action='store_true',
+        help='Build windows portable'
+    )
     return parser
 
 
@@ -187,6 +192,20 @@ def build_flutter_arch_manjaro():
     os.chdir('..')
     os.system('HBB=`pwd` FLUTTER=1 makepkg -f')
 
+def build_flutter_windows_portable():
+    os.system("cargo build --lib --features flutter --release")
+    os.chdir('flutter')
+    os.system("flutter build windows --release")
+    os.chdir('..')
+    os.chdir("libs/portable")
+    os.system("pip3 install -r requirements.txt")
+    os.system("python3 .\\generate.py -f ..\\..\\flutter\\build\\windows\\runner\Release\ -o . -e ..\\..\\flutter\\build\\windows\\runner\\Release\\rustdesk.exe")
+    os.chdir("../..")
+    if os.path.exists("./rustdesk_portable.exe"):
+        os.replace("./target/release/rustdesk-portable-packer.exe", "./rustdesk_portable.exe")
+    else:
+        os.rename("./target/release/rustdesk-portable-packer.exe", "./rustdesk_portable.exe")
+    print(f"output location: {os.path.abspath(os.curdir)}/rustdesk_portable.exe")
 
 def main():
     parser = make_parser()
@@ -201,13 +220,19 @@ def main():
                 '//#![windows_subsystem', '#![windows_subsystem'))
     if os.path.exists(exe_path):
         os.unlink(exe_path)
-    os.system('python3 res/inline-sciter.py')
     if os.path.isfile('/usr/bin/pacman'):
         os.system('git checkout src/ui/common.tis')
     version = get_version()
     features = ",".join(get_features(args))
     flutter = args.flutter
+    if not flutter:
+        # not flutter, is sciter
+        os.system('python3 res/inline-sciter.py')
+    portable = args.portable
     if windows:
+        if portable:
+            build_flutter_windows_portable()
+            return
         os.system('cargo build --release --features ' + features)
         # os.system('upx.exe target/release/rustdesk.exe')
         os.system('mv target/release/rustdesk.exe target/release/RustDesk.exe')
diff --git a/libs/portable/.gitignore b/libs/portable/.gitignore
new file mode 100644
index 000000000..8dfaeb73d
--- /dev/null
+++ b/libs/portable/.gitignore
@@ -0,0 +1,3 @@
+/target
+*.exe
+*.bin
\ No newline at end of file
diff --git a/libs/portable/Cargo.lock b/libs/portable/Cargo.lock
new file mode 100644
index 000000000..623d64918
--- /dev/null
+++ b/libs/portable/Cargo.lock
@@ -0,0 +1,285 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "alloc-no-stdlib"
+version = "2.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
+
+[[package]]
+name = "alloc-stdlib"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
+dependencies = [
+ "alloc-no-stdlib",
+]
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "brotli"
+version = "3.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+ "brotli-decompressor",
+]
+
+[[package]]
+name = "brotli-decompressor"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80"
+dependencies = [
+ "alloc-no-stdlib",
+ "alloc-stdlib",
+]
+
+[[package]]
+name = "cc"
+version = "1.0.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "dirs"
+version = "4.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.3.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
+dependencies = [
+ "libc",
+ "redox_users",
+ "winapi",
+]
+
+[[package]]
+name = "embed-resource"
+version = "1.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "936c1354206a875581696369aef920e12396e93bbd251c43a7a3f3fa85023a7d"
+dependencies = [
+ "cc",
+ "rustc_version",
+ "toml",
+ "vswhom",
+ "winreg",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.133"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966"
+
+[[package]]
+name = "md5"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.43"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a2ca2c61bc9f3d74d2886294ab7b9853abd9c1ad903a3ac7815c58989bb7bab"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
+dependencies = [
+ "getrandom",
+ "redox_syscall",
+ "thiserror",
+]
+
+[[package]]
+name = "rustc_version"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustdesk-portable-packer"
+version = "0.1.0"
+dependencies = [
+ "brotli",
+ "dirs",
+ "embed-resource",
+ "md5",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4"
+
+[[package]]
+name = "serde"
+version = "1.0.144"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f747710de3dcd43b88c9168773254e809d8ddbdf9653b84e2554ab219f17860"
+
+[[package]]
+name = "syn"
+version = "1.0.100"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "toml"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd"
+
+[[package]]
+name = "vswhom"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
+dependencies = [
+ "libc",
+ "vswhom-sys",
+]
+
+[[package]]
+name = "vswhom-sys"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22025f6d8eb903ebf920ea6933b70b1e495be37e2cb4099e62c80454aaf57c39"
+dependencies = [
+ "cc",
+ "libc",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "winreg"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
+dependencies = [
+ "winapi",
+]
diff --git a/libs/portable/Cargo.toml b/libs/portable/Cargo.toml
new file mode 100644
index 000000000..bdd7b23ca
--- /dev/null
+++ b/libs/portable/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "rustdesk-portable-packer"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+build = "build.rs"
+
+[build-dependencies]
+embed-resource = "1.7"
+
+[dependencies]
+brotli = "3.3.4"
+dirs = "4.0.0"
+md5 = "0.7.0"
diff --git a/libs/portable/build.rs b/libs/portable/build.rs
new file mode 100644
index 000000000..2fedd1d9c
--- /dev/null
+++ b/libs/portable/build.rs
@@ -0,0 +1,5 @@
+extern crate embed_resource;
+
+fn main() {
+    embed_resource::compile("icon.rc");
+}
diff --git a/libs/portable/generate.py b/libs/portable/generate.py
new file mode 100644
index 000000000..640f2ae6a
--- /dev/null
+++ b/libs/portable/generate.py
@@ -0,0 +1,88 @@
+from ast import parse
+import os
+import optparse
+from hashlib import md5
+import brotli
+
+# file compress level(0-11)
+compress_level = 11
+# 4GB maximum
+length_count = 4
+# encoding
+encoding = 'utf-8'
+
+# output: {path: (compressed_data, file_md5)}
+
+
+def generate_md5_table(folder: str) -> dict:
+    res: dict = dict()
+    curdir = os.curdir
+    os.chdir(folder)
+    for root, _, files in os.walk('.'):
+        # remove ./
+        for f in files:
+            md5_generator = md5()
+            full_path = os.path.join(root, f)
+            print(f"processing {full_path}...")
+            f = open(full_path, "rb")
+            content = f.read()
+            content_compressed = brotli.compress(
+                content, quality=compress_level)
+            md5_generator.update(content)
+            md5_code = md5_generator.hexdigest().encode(encoding=encoding)
+            res[full_path] = (content_compressed, md5_code)
+    os.chdir(curdir)
+    return res
+
+
+def write_metadata(md5_table: dict, output_folder: str, exe: str):
+    output_path = os.path.join(output_folder, "data.bin")
+    with open(output_path, "wb") as f:
+        f.write("rustdesk".encode(encoding=encoding))
+        for path in md5_table.keys():
+            (compressed_data, md5_code) = md5_table[path]
+            data_length = len(compressed_data)
+            path = path.encode(encoding=encoding)
+            # path length & path
+            f.write((len(path)).to_bytes(length=length_count, byteorder='big'))
+            f.write(path)
+            # data length & compressed data
+            f.write((data_length).to_bytes(
+                length=length_count, byteorder='big'))
+            f.write(compressed_data)
+            # md5 code
+            f.write(md5_code)
+        # end
+        f.write("rustdesk".encode(encoding=encoding))
+        # executable
+        f.write(exe.encode(encoding='utf-8'))
+    print(f"metadata had written to {output_path}")
+
+
+def build_portable(output_folder: str):
+    os.chdir(output_folder)
+    os.system("cargo build --release")
+
+# Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py
+# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe
+if __name__ == '__main__':
+    parser = optparse.OptionParser()
+    parser.add_option("-f", "--folder", dest="folder",
+                      help="folder to compress")
+    parser.add_option("-o", "--output", dest="output_folder",
+                      help="the root of portable packer project")
+    parser.add_option("-e", "--executable", dest="executable",
+                      help="specify startup file")
+    (options, args) = parser.parse_args()
+    folder = options.folder
+    output_folder = os.path.abspath(options.output_folder)
+
+    exe: str = os.path.abspath(options.executable)
+    if not exe.startswith(os.path.abspath(folder)):
+        print("the executable must locate in source folder")
+        exit(-1)
+    exe = '.' + exe[len(os.path.abspath(folder)):]
+    print("executable path: " + exe)
+    md5_table = generate_md5_table(folder)
+    write_metadata(md5_table, output_folder, exe)
+    build_portable(output_folder)
diff --git a/libs/portable/icon.rc b/libs/portable/icon.rc
new file mode 100644
index 000000000..2f41e79d8
--- /dev/null
+++ b/libs/portable/icon.rc
@@ -0,0 +1 @@
+rustdesk_icon ICON "../../res/icon.ico"
\ No newline at end of file
diff --git a/libs/portable/requirements.txt b/libs/portable/requirements.txt
new file mode 100644
index 000000000..ac6cebc82
--- /dev/null
+++ b/libs/portable/requirements.txt
@@ -0,0 +1 @@
+brotli
\ No newline at end of file
diff --git a/libs/portable/src/bin_reader.rs b/libs/portable/src/bin_reader.rs
new file mode 100644
index 000000000..499c18e2c
--- /dev/null
+++ b/libs/portable/src/bin_reader.rs
@@ -0,0 +1,134 @@
+use std::{
+    fs::{self},
+    io::{Cursor, Read},
+    path::PathBuf,
+};
+
+const BIN_DATA: &[u8] = include_bytes!("../data.bin");
+// 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).unwrap();
+        buf
+    }
+
+    pub fn write_to_file(&self, prefix: &PathBuf) {
+        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();
+            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 vaild!");
+        }
+        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(unix)]
+    pub fn configure_permission(&self, prefix: &PathBuf) {
+        use std::os::unix::prelude::PermissionsExt;
+
+        let exe_path = prefix.join(&self.exe);
+        if exe_path.exists() {
+            let f = File::open(exe_path).unwrap();
+            let meta = f.metadata().unwrap();
+            let mut permissions = meta.permissions();
+            permissions.set_mode(0o755);
+            f.set_permissions(permissions).unwrap();
+        }
+    }
+}
diff --git a/libs/portable/src/main.rs b/libs/portable/src/main.rs
new file mode 100644
index 000000000..614c4c17c
--- /dev/null
+++ b/libs/portable/src/main.rs
@@ -0,0 +1,51 @@
+#![windows_subsystem = "windows"]
+
+use std::{
+    path::PathBuf,
+    process::{Command, Stdio},
+};
+
+use bin_reader::BinaryReader;
+
+pub mod bin_reader;
+
+const APP_PREFIX: &str = "rustdesk";
+const APPNAME_RUNTIME_ENV_KEY: &str = "RUSTDESK_APPNAME";
+
+fn setup(reader: BinaryReader) -> Option<PathBuf> {
+    // home dir
+    if let Some(dir) = dirs::data_local_dir() {
+        let dir = dir.join(APP_PREFIX);
+        for file in reader.files.iter() {
+            file.write_to_file(&dir);
+        }
+        #[cfg(unix)]
+        reader.configure_permission(&dir);
+        Some(dir.join(&reader.exe))
+    } else {
+        eprintln!("not found data local dir");
+        None
+    }
+}
+
+fn execute(path: PathBuf) {
+    println!("executing {}", path.display());
+    // setup env
+    let exe = std::env::current_exe().unwrap();
+    let exe_name = exe.file_name().unwrap();
+    // run executable
+    Command::new(path)
+        .env(APPNAME_RUNTIME_ENV_KEY, exe_name)
+        .stdin(Stdio::inherit())
+        .stdout(Stdio::inherit())
+        .stderr(Stdio::inherit())
+        .output()
+        .expect(&format!("failed to execute {:?}", exe_name));
+}
+
+fn main() {
+    let reader = BinaryReader::default();
+    if let Some(exe) = setup(reader) {
+        execute(exe);
+    }
+}