feat(windows): Tauri boilerplate and CI changes (#2742)

Trying to get CI/CD to produce firezone-windows-client.exe. Can't
remember if I need both a PR and a draft release or just the draft
release for that.

---------

Signed-off-by: Reactor Scram <ReactorScram@users.noreply.github.com>
Co-authored-by: Jamil <jamilbk@users.noreply.github.com>
This commit is contained in:
Reactor Scram
2023-11-30 19:50:43 +00:00
committed by GitHub
parent 55e8d3407f
commit 189a35f692
20 changed files with 2918 additions and 168 deletions

View File

@@ -64,6 +64,81 @@ jobs:
- uses: ./.github/actions/setup-rust
- run: cargo test --all-features ${{ matrix.packages }}
# TODO: Remove when windows build works reliably in cd.yml
# These `temp-` jobs serve as a sanity check to early exit if there's an issue in the workflow
temp-build-windows-artifacts:
runs-on: windows-2019
defaults:
run:
working-directory: ./rust
strategy:
fail-fast: false
# The matrix is 1x1 to match the style of build-push-linux-release-artifacts
# In the future we could try to cross-compile aarch64-windows here.
matrix:
name:
- package: hello-world
artifact: hello-world
env:
BINARY_DEST_PATH: ${{ matrix.name.artifact }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-rust
with:
targets: x86_64-pc-windows-msvc
- name: Build release binaries
run: |
cargo build --release -p ${{ matrix.name.package }}
cp target/release/${{ matrix.name.package }}.exe "${{ env.BINARY_DEST_PATH }}-x64.exe"
pwd
ls *.exe
- name: Save build artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.name.artifact }}-x64.exe
path: ${{ github.workspace }}/rust/${{ env.BINARY_DEST_PATH }}-x64.exe
# This should be identical to `build-push-windows-release-artifacts` in `cd.yml` except for the permissions, needs, and uploading step
temp-build-tauri:
runs-on: windows-2019
defaults:
run:
working-directory: ./rust
strategy:
fail-fast: false
# The matrix is 1x1 to match the style of build-push-linux-release-artifacts
# In the future we could try to cross-compile aarch64-windows here.
matrix:
name:
- package: firezone-windows-client
artifact: windows-client
env:
BINARY_DEST_PATH: ${{ matrix.name.artifact }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-rust
with:
targets: x86_64-pc-windows-msvc
- name: Build release binaries
run: |
cargo install tauri-cli
# Tauri build already defaults to '--release'
cargo tauri build
# Used for release artifact
cp target/release/${{ matrix.name.package }}.exe "${{ env.BINARY_DEST_PATH }}-x64.exe"
pwd
ls *.exe
# There's an MSI and NSIS installer here but their names include the version so I'll try those later.
- name: Save build artifacts
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.name.artifact }}-x64.exe
path: ${{ github.workspace }}/rust/${{ env.BINARY_DEST_PATH }}-x64.exe
smoke-test-relay:
runs-on: ubuntu-22.04
defaults:

View File

@@ -243,59 +243,54 @@ jobs:
if-no-files-found: error
retention-days: 1
# Build for Windows with Cross
# Build for Windows
build-push-windows-release-artifacts:
permissions:
id-token: write
contents: write
needs: update-release-draft
runs-on: ubuntu-22.04
runs-on: windows-2019
defaults:
run:
working-directory: ./rust
strategy:
fail-fast: false
# The matrix is 1x1 to match the style of build-push-linux-release-artifacts
# In the future we could try to cross-compile aarch64-windows here.
matrix:
arch:
- target: x86_64-pc-windows-msvc
shortname: x64
name:
- package: firezone-windows-client
artifact: windows-client
env:
BINARY_DEST_PATH: ${{ matrix.name.artifact }}-${{ matrix.arch.shortname }}
BINARY_DEST_PATH: ${{ matrix.name.artifact }}
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-rust
with:
targets: x86_64-pc-windows-msvc
- uses: taiki-e/install-action@v2
with:
tool: cross
- name: Build release binaries
run: |
set -xe
cargo install tauri-cli
cross build --release -p ${{ matrix.name.package }} --target ${{ matrix.arch.target }}
# Tauri build already defaults to '--release'
cargo tauri build
# Used for release artifact
cp target/${{ matrix.arch.target }}/release/${{ matrix.name.package }} $BINARY_DEST_PATH
cp target/release/${{ matrix.name.package }}.exe "${{ env.BINARY_DEST_PATH }}-x64.exe"
pwd
ls *.exe
# There's an MSI and NSIS installer here but their names include the version so I'll try those later.
# Used for Docker images
cp target/${{ matrix.arch.target }}/release/${{ matrix.name.package }} ${{ matrix.name.package }}
sha256sum $BINARY_DEST_PATH > $BINARY_DEST_PATH.sha256sum.txt
ls -la $BINARY_DEST_PATH
ls -la $BINARY_DEST_PATH.sha256sum.txt
- name: Upload Release Assets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -xe
# TODO: Calculate sha256sum for this binary and attach to release
gh release upload ${{ needs.update-release-draft.outputs.tag_name }} \
${{ env.BINARY_DEST_PATH }} \
${{ env.BINARY_DEST_PATH }}.sha256sum.txt \
${{ env.BINARY_DEST_PATH }}-x64.exe \
--clobber \
--repo ${{ github.repository }}

2358
rust/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,11 +6,12 @@ members = [
"connlib/shared",
"connlib/tunnel",
"gateway",
"hello-world",
"linux-client",
"firezone-cli-utils",
"phoenix-channel",
"relay",
"windows-client",
"windows-client/src-tauri",
]
resolver = "2"
@@ -32,7 +33,7 @@ connlib-client-apple = { path = "connlib/clients/apple"}
connlib-client-shared = { path = "connlib/clients/shared"}
firezone-gateway = { path = "gateway"}
firezone-linux-client = { path = "linux-client"}
firezone-windows-client = { path = "windows-client"}
firezone-windows-client = { path = "windows-client/src-tauri"}
firezone-cli-utils = { path = "firezone-cli-utils"}
connlib-shared = { path = "connlib/shared"}
firezone-tunnel = { path = "connlib/tunnel"}

8
rust/hello-world/Cargo.toml Executable file
View File

@@ -0,0 +1,8 @@
[package]
name = "hello-world"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]

3
rust/hello-world/src/main.rs Executable file
View File

@@ -0,0 +1,3 @@
fn main() {
println!("Hello, world!");
}

23
rust/windows-client/.gitignore vendored Executable file
View File

@@ -0,0 +1,23 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

View File

@@ -1,22 +0,0 @@
[package]
name = "firezone-windows-client"
# mark:automatic-version
version = "1.20231001.0"
edition = "2021"
[target.'cfg(windows)'.build-dependencies]
embed-resource = "2.4.0"
[target.'cfg(windows)'.dependencies]
native-windows-derive = "1.0.5"
native-windows-gui = "1.0.13"
[target.'cfg(windows)'.dependencies.windows]
version = "0.52.0"
features = [
"Win32_Foundation",
"Win32_Graphics_Gdi",
"Win32_System_LibraryLoader",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
]

View File

@@ -1,11 +1,46 @@
# windows-client
This crate houses the Firezone Windows client.
This crate houses a Windows GUI client.
## Building
Run `cargo build` in this directory.
From this dir:
```
# First-time setup - Install Tauri's dev server / hot-reload tool
cargo install tauri-cli
# Builds a release exe
cargo tauri build
# The release exe, MSI, and NSIS installer should be up in the workspace.
# The exe can run without being installed
stat ../target/release/firezone-windows-client.exe
stat ../target/release/bundle/msi/firezone-windows-client_0.0.0_x64_en-US.msi
stat ../target/release/bundle/nsis/firezone-windows-client_0.0.0_x64-setup.exe
```
## Running
Run `cargo run` in this directory.
From this dir:
```
# Tauri has some hot-reloading features. If the Rust code changes it will even recompile and restart the program for you.
cargo tauri dev
# You can call debug subcommands on the exe from this directory too
# e.g. this is equivalent to `cargo run -- debug`
cargo tauri dev -- -- debug
# Debug connlib GUI integration
cargo tauri dev -- -- debug-connlib
# The exe is up in the workspace
stat ../target/debug/firezone-windows-client.exe
```
## Recommended IDE Setup
(From Tauri's default README)
- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer)

View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Usage: ./ci_check.bash
# Performs static checks similar to what the Github Actions workflows will do, so errors can be caught before committing.
# ReactorScram uses this in the Git pre-commit hook on her Windows dev system.
# Fail on any non-zero return code
set -euo pipefail
# Fail on yaml workflow errors
yamllint ../../.github/workflows/*
# Fail on Rust errors
pushd .. > /dev/null
cargo clippy --all-targets --all-features -p firezone-windows-client -- -D warnings
cargo fmt --check
cargo doc --all-features --no-deps --document-private-items -p firezone-windows-client
popd > /dev/null

3
rust/windows-client/src-tauri/.gitignore vendored Executable file
View File

@@ -0,0 +1,3 @@
# Generated by Cargo
# will have compiled files and executables
/target/

View File

@@ -0,0 +1,35 @@
[package]
name = "firezone-windows-client"
# mark:automatic-version
version = "1.20231001.0"
description = "Firezone Windows Client"
edition = "2021"
[build-dependencies]
tauri-build = { version = "1.5", features = [] }
[dependencies]
connlib-client-shared = { workspace = true }
secrecy.workspace = true
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0.50"
[target.'cfg(windows)'.dependencies]
# Tauri works fine on Linux, but it requires a lot of build-time deps like glib and gdk, so I've blocked it out for now.
tauri = { version = "1.5", features = [ "system-tray", "shell-open"] }
wintun = "0.3.2"
[target.'cfg(windows)'.dependencies.windows]
version = "0.52.0"
features = [
"Win32_Foundation",
"Win32_System_LibraryLoader",
"Win32_UI_Shell",
"Win32_UI_WindowsAndMessaging",
]
[features]
# this feature is used for production builds or when `devPath` points to the filesystem
# DO NOT REMOVE!!
custom-protocol = ["tauri/custom-protocol"]

View File

@@ -0,0 +1,3 @@
fn main() {
tauri_build::build()
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@@ -0,0 +1,237 @@
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use thiserror::Error;
#[derive(Error, Debug)]
enum Error {}
fn main() {
let mut args = std::env::args();
// Ignore the exe name
args.next().unwrap();
match args.next().as_deref() {
None | Some("tauri") => details::main_tauri(),
Some("debug") => println!("debug"),
Some("debug-connlib") => main_debug_connlib(),
Some("debug-wintun") => details::main_debug_wintun(),
Some(cmd) => println!("Subcommand `{cmd}` not recognized"),
}
}
fn main_debug_connlib() {
use connlib_client_shared::Error as ConnlibError;
use connlib_client_shared::{Callbacks, Session};
use std::str::FromStr;
#[derive(Clone, Default)]
struct WindowsCallbacks {}
impl Callbacks for WindowsCallbacks {
type Error = Error;
fn on_disconnect(&self, error: Option<&ConnlibError>) -> Result<(), Self::Error> {
panic!("error recovery not implemented. Error: {error:?}");
}
fn on_error(&self, error: &ConnlibError) -> Result<(), Self::Error> {
panic!("error recovery not implemented. Error: {error}");
}
}
let callbacks = WindowsCallbacks::default();
let _session = Session::connect(
"https://api.firez.one/firezone",
secrecy::SecretString::from_str("bogus_secret").unwrap(),
"trisha-laptop-2023".to_string(),
callbacks,
);
}
#[cfg(target_os = "linux")]
mod details {
pub fn main_tauri() {
panic!("GUI not implemented for Linux.");
}
pub fn main_debug_wintun() {
panic!("Wintun not implemented for Linux.");
}
}
#[cfg(target_os = "windows")]
mod details {
use tauri::{
CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
SystemTraySubmenu,
};
pub fn main_tauri() {
let tray = SystemTray::new().with_menu(signed_out_menu());
tauri::Builder::default()
.on_window_event(|event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
// Keep the frontend running but just hide this webview
// Per https://tauri.app/v1/guides/features/system-tray/#preventing-the-app-from-closing
event.window().hide().unwrap();
api.prevent_close();
}
})
.invoke_handler(tauri::generate_handler![greet])
.system_tray(tray)
.on_system_tray_event(|app, event| {
if let SystemTrayEvent::MenuItemClick { id, .. } = event {
match id.as_str() {
"/sign_in" => {
app.tray_handle()
.set_menu(signed_in_menu(
"user@example.com",
&[("CloudFlare", "1.1.1.1"), ("Google", "8.8.8.8")],
))
.unwrap();
}
"/sign_out" => app.tray_handle().set_menu(signed_out_menu()).unwrap(),
"/about" => {
let win = app.get_window("main-window").unwrap();
if win.is_visible().unwrap() {
// If we close the window here, we can't re-open it, we'd have to fully re-create it. Not needed for MVP - We agreed 100 MB is fine for the GUI client.
win.hide().unwrap();
} else {
win.show().unwrap();
}
}
"/settings" => {
app.tray_handle()
.set_menu(signed_in_menu(
"user@example.com",
&[
("CloudFlare", "1.1.1.1"),
("New resource", "127.0.0.1"),
("Google", "8.8.8.8"),
],
))
.unwrap();
}
"/quit" => app.exit(0),
id => {
if let Some(addr) = id.strip_prefix("/resource/") {
println!("TODO copy {addr} to clipboard");
}
}
}
}
})
.build(tauri::generate_context!())
.expect("error while building tauri application")
.run(|_app_handle, event| {
if let tauri::RunEvent::ExitRequested { api, .. } = event {
// Don't exit if we close our main window
// https://tauri.app/v1/guides/features/system-tray/#preventing-the-app-from-closing
api.prevent_exit();
}
});
}
pub fn main_debug_wintun() {
use std::sync::Arc;
//Must be run as Administrator because we create network adapters
//Load the wintun dll file so that we can call the underlying C functions
//Unsafe because we are loading an arbitrary dll file
let wintun = unsafe { wintun::load_from_path("../wintun/bin/amd64/wintun.dll") }
.expect("Failed to load wintun dll");
//Try to open an adapter with the name "Demo"
let adapter = match wintun::Adapter::open(&wintun, "Demo") {
Ok(a) => a,
Err(_) => {
//If loading failed (most likely it didn't exist), create a new one
wintun::Adapter::create(&wintun, "Demo", "Example manor hatch stash", None)
.expect("Failed to create wintun adapter!")
}
};
//Specify the size of the ring buffer the wintun driver should use.
let session = Arc::new(adapter.start_session(wintun::MAX_RING_CAPACITY).unwrap());
//Get a 20 byte packet from the ring buffer
let mut packet = session.allocate_send_packet(20).unwrap();
let bytes: &mut [u8] = packet.bytes_mut();
//Write IPV4 version and header length
bytes[0] = 0x40;
//Finish writing IP header
bytes[9] = 0x69;
bytes[10] = 0x04;
bytes[11] = 0x20;
//...
//Send the packet to wintun virtual adapter for processing by the system
session.send_packet(packet);
println!("Sleeping 1 minute, see if the adapter is visible...");
std::thread::sleep(std::time::Duration::from_secs(60));
//Stop any readers blocking for data on other threads
//Only needed when a blocking reader is preventing shutdown Ie. it holds an Arc to the
//session, blocking it from being dropped
session.shutdown().unwrap();
//the session is stopped on drop
//drop(session);
//drop(adapter)
//And the adapter closes its resources when dropped
}
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
fn signed_in_menu(user_email: &str, resources: &[(&str, &str)]) -> SystemTrayMenu {
let mut menu = SystemTrayMenu::new()
.add_item(
CustomMenuItem::new("".to_string(), format!("Signed in as {user_email}"))
.disabled(),
)
.add_item(CustomMenuItem::new("/sign_out".to_string(), "Sign out"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("".to_string(), "RESOURCES"));
for (name, addr) in resources {
let submenu = SystemTrayMenu::new().add_item(CustomMenuItem::new(
format!("/resource/{addr}"),
addr.to_string(),
));
menu = menu.add_submenu(SystemTraySubmenu::new(name.to_string(), submenu));
}
menu = menu
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("/about".to_string(), "About"))
.add_item(CustomMenuItem::new("/settings".to_string(), "Settings"))
.add_item(
CustomMenuItem::new("/quit".to_string(), "Quit Firezone").accelerator("Ctrl+Q"),
);
menu
}
fn signed_out_menu() -> SystemTrayMenu {
SystemTrayMenu::new()
.add_item(CustomMenuItem::new("/sign_in".to_string(), "Sign In"))
.add_native_item(SystemTrayMenuItem::Separator)
.add_item(CustomMenuItem::new("/about".to_string(), "About"))
.add_item(CustomMenuItem::new("/settings".to_string(), "Settings"))
.add_item(
CustomMenuItem::new("/quit".to_string(), "Quit Firezone").accelerator("Ctrl+Q"),
)
}
}

View File

@@ -0,0 +1,47 @@
{
"build": {
"beforeDevCommand": "",
"beforeBuildCommand": "",
"devPath": "../src",
"distDir": "../src",
"withGlobalTauri": true
},
"package": {
"productName": "firezone-windows-client",
"version": "0.0.0"
},
"tauri": {
"allowlist": {
"all": false,
"shell": {
"all": false,
"open": true
}
},
"bundle": {
"active": true,
"targets": "all",
"identifier": "dev.firezone",
"icon": [
"icons/firezone.ico"
]
},
"security": {
"csp": null
},
"systemTray": {
"iconPath": "icons/firezone.ico",
"iconAsTemplate": true
},
"windows": [
{
"fullscreen": false,
"resizable": true,
"label": "main-window",
"title": "firezone-windows-client",
"width": 640,
"height": 480
}
]
}
}

View File

@@ -0,0 +1,42 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="stylesheet" href="styles.css" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Tauri App</title>
<script type="module" src="/main.js" defer></script>
<style>
.logo.vanilla:hover {
filter: drop-shadow(0 0 2em #ffe21c);
}
</style>
</head>
<body>
<div class="container">
<h1>Welcome to Firezone!</h1>
<div class="row">
<a href="https://www.firezone.dev/?utm_source=windows-client" target="_blank">
Tauri logo
</a>
<a
href="https://developer.mozilla.org/en-US/docs/Web/JavaScript"
target="_blank"
>
JavaScript logo
</a>
</div>
<p>Click on the Tauri logo to learn more about Firezone</p>
<form class="row" id="greet-form">
<input id="greet-input" placeholder="Enter a name..." />
<button type="submit">Greet</button>
</form>
<p id="greet-msg"></p>
</div>
</body>
</html>

18
rust/windows-client/src/main.js Executable file
View File

@@ -0,0 +1,18 @@
const { invoke } = window.__TAURI__.tauri;
let greetInputEl;
let greetMsgEl;
async function greet() {
// Learn more about Tauri commands at https://tauri.app/v1/guides/features/command
greetMsgEl.textContent = await invoke("greet", { name: greetInputEl.value });
}
window.addEventListener("DOMContentLoaded", () => {
greetInputEl = document.querySelector("#greet-input");
greetMsgEl = document.querySelector("#greet-msg");
document.querySelector("#greet-form").addEventListener("submit", (e) => {
e.preventDefault();
greet();
});
});

View File

@@ -1 +0,0 @@
fn main() {}

View File

@@ -0,0 +1,109 @@
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color: #0f0f0f;
background-color: #f6f6f6;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
.container {
margin: 0;
padding-top: 10vh;
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
transition: 0.75s;
}
.logo.tauri:hover {
filter: drop-shadow(0 0 2em #24c8db);
}
.row {
display: flex;
justify-content: center;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
h1 {
text-align: center;
}
input,
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
color: #0f0f0f;
background-color: #ffffff;
transition: border-color 0.25s;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.2);
}
button {
cursor: pointer;
}
button:hover {
border-color: #396cd8;
}
button:active {
border-color: #396cd8;
background-color: #e8e8e8;
}
input,
button {
outline: none;
}
#greet-input {
margin-right: 5px;
}
@media (prefers-color-scheme: dark) {
:root {
color: #f6f6f6;
background-color: #2f2f2f;
}
a:hover {
color: #24c8db;
}
input,
button {
color: #ffffff;
background-color: #0f0f0f98;
}
button:active {
background-color: #0f0f0f69;
}
}