Skip to content

Commit 7fbe440

Browse files
Desktop: Bundle for Mac and Windows (#3297)
* add bundle for mac os and windows * Fix bundle name This name gets used as the display name of the app on mac os, shorter name looks better and is more consistent. * preserve std out by running bin directly * bundle placeholder on linux * fix linux
1 parent 557df69 commit 7fbe440

File tree

21 files changed

+429
-15
lines changed

21 files changed

+429
-15
lines changed

.cargo/config.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@ rustflags = [
1010
"link-arg=--max-memory=4294967296",
1111
"--cfg=web_sys_unstable_apis",
1212
]
13+
14+
[env]
15+
CARGO_WORKSPACE_DIR = { value = "", relative = true }

Cargo.lock

Lines changed: 54 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ members = [
33
"desktop",
44
"desktop/wrapper",
55
"desktop/embedded-resources",
6+
"desktop/bundle",
7+
"desktop/platform/linux",
8+
"desktop/platform/mac",
9+
"desktop/platform/win",
610
"editor",
711
"frontend/wasm",
812
"libraries/dyn-any",

desktop/Cargo.toml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,3 @@ core-foundation = { version = "0.10", optional = true }
7373
# Linux-specific dependencies
7474
[target.'cfg(target_os = "linux")'.dependencies]
7575
libc = { version = "0.2", optional = true }
76-
77-
[target.'cfg(target_os = "windows")'.build-dependencies]
78-
winres = "0.1"

desktop/bundle/Cargo.toml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[package]
2+
name = "graphite-desktop-bundle"
3+
version = "0.0.0"
4+
description = "Graphite Desktop Bundle"
5+
authors = ["Graphite Authors <contact@graphite.rs>"]
6+
license = "Apache-2.0"
7+
repository = ""
8+
edition = "2024"
9+
rust-version = "1.87"
10+
11+
[dependencies]
12+
cef-dll-sys = { workspace = true }
13+
14+
[target.'cfg(target_os = "macos")'.dependencies]
15+
serde = { workspace = true }
16+
plist = { version = "*" }

desktop/bundle/build.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
fn main() {
2+
println!("cargo:rerun-if-env-changed=CARGO_PROFILE");
3+
println!("cargo:rerun-if-env-changed=PROFILE");
4+
let profile = std::env::var("CARGO_PROFILE").or_else(|_| std::env::var("PROFILE")).unwrap();
5+
println!("cargo:rustc-env=CARGO_PROFILE={profile}");
6+
7+
println!("cargo:rerun-if-env-changed=DEP_CEF_DLL_WRAPPER_CEF_DIR");
8+
let cef_dir = std::env::var("DEP_CEF_DLL_WRAPPER_CEF_DIR").unwrap();
9+
println!("cargo:rustc-env=CEF_PATH={cef_dir}");
10+
}

desktop/bundle/src/common.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
use std::error::Error;
2+
use std::fs;
3+
use std::path::{Path, PathBuf};
4+
use std::process::{Command, Stdio};
5+
6+
pub(crate) const APP_NAME: &str = "Graphite";
7+
8+
fn profile_name() -> &'static str {
9+
let mut profile = env!("CARGO_PROFILE");
10+
if profile == "debug" {
11+
profile = "dev";
12+
}
13+
profile
14+
}
15+
16+
pub(crate) fn profile_path() -> PathBuf {
17+
PathBuf::from(env!("CARGO_WORKSPACE_DIR")).join(format!("target/{}", env!("CARGO_PROFILE")))
18+
}
19+
20+
pub(crate) fn cef_path() -> PathBuf {
21+
PathBuf::from(env!("CEF_PATH"))
22+
}
23+
24+
pub(crate) fn build_bin(package: &str, bin: Option<&str>) -> Result<PathBuf, Box<dyn Error>> {
25+
let profile = &profile_name();
26+
let mut args = vec!["build", "--package", package, "--profile", profile];
27+
if let Some(bin) = bin {
28+
args.push("--bin");
29+
args.push(bin);
30+
}
31+
run_command("cargo", &args)?;
32+
let profile_path = profile_path();
33+
let mut bin_path = if let Some(bin) = bin { profile_path.join(bin) } else { profile_path.join(package) };
34+
if cfg!(target_os = "windows") {
35+
bin_path.set_extension("exe");
36+
}
37+
Ok(bin_path)
38+
}
39+
40+
pub(crate) fn run_command(program: &str, args: &[&str]) -> Result<(), Box<dyn std::error::Error>> {
41+
let status = Command::new(program).args(args).stdout(Stdio::inherit()).stderr(Stdio::inherit()).status()?;
42+
if !status.success() {
43+
std::process::exit(1);
44+
}
45+
Ok(())
46+
}
47+
48+
pub(crate) fn clean_dir(dir: &Path) {
49+
if dir.exists() {
50+
fs::remove_dir_all(dir).unwrap();
51+
}
52+
fs::create_dir_all(dir).unwrap();
53+
}
54+
55+
pub(crate) fn copy_dir(src: &Path, dst: &Path) {
56+
fs::create_dir_all(dst).unwrap();
57+
for entry in fs::read_dir(src).unwrap() {
58+
let entry = entry.unwrap();
59+
let dst_path = dst.join(entry.file_name());
60+
if entry.file_type().unwrap().is_dir() {
61+
copy_dir(&entry.path(), &dst_path);
62+
} else {
63+
fs::copy(entry.path(), &dst_path).unwrap();
64+
}
65+
}
66+
}

desktop/bundle/src/linux.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
use std::error::Error;
2+
3+
use crate::common::*;
4+
5+
const PACKAGE: &str = "graphite-desktop-platform-linux";
6+
7+
pub fn main() -> Result<(), Box<dyn Error>> {
8+
let app_bin = build_bin(PACKAGE, None)?;
9+
10+
// TODO: Implement bundling for linux
11+
12+
// TODO: Consider adding more useful cli
13+
if std::env::args().any(|a| a == "open") {
14+
run_command(&app_bin.to_string_lossy(), &[]).expect("failed to open app");
15+
} else {
16+
println!("Binary built and placed at {}", app_bin.to_string_lossy());
17+
eprintln!("Bundling for Linux is not yet implemented.");
18+
eprintln!("You can still start the app with the `open` subcommand. `cargo run -p graphite-desktop-bundle -- open`");
19+
std::process::exit(1);
20+
}
21+
22+
Ok(())
23+
}

desktop/bundle/src/mac.rs

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
use std::collections::HashMap;
2+
use std::error::Error;
3+
use std::fs;
4+
use std::path::{Path, PathBuf};
5+
6+
use crate::common::*;
7+
8+
const APP_ID: &str = "rs.graphite.GraphiteEditor";
9+
10+
const PACKAGE: &str = "graphite-desktop-platform-mac";
11+
const HELPER_BIN: &str = "graphite-desktop-platform-mac-helper";
12+
13+
const EXEC_PATH: &str = "Contents/MacOS";
14+
const FRAMEWORKS_PATH: &str = "Contents/Frameworks";
15+
const RESOURCES_PATH: &str = "Contents/Resources";
16+
const FRAMEWORK: &str = "Chromium Embedded Framework.framework";
17+
18+
pub fn main() -> Result<(), Box<dyn Error>> {
19+
let app_bin = build_bin(PACKAGE, None)?;
20+
let helper_bin = build_bin(PACKAGE, Some(HELPER_BIN))?;
21+
22+
let profile_path = profile_path();
23+
let app_dir = bundle(&profile_path, &app_bin, &helper_bin);
24+
25+
// TODO: Consider adding more useful cli
26+
if std::env::args().any(|a| a == "open") {
27+
let executable_path = app_dir.join(EXEC_PATH).join(APP_NAME);
28+
run_command(&executable_path.to_string_lossy(), &[]).expect("failed to open app");
29+
}
30+
31+
Ok(())
32+
}
33+
34+
fn bundle(out_dir: &Path, app_bin: &Path, helper_bin: &Path) -> PathBuf {
35+
let app_dir = out_dir.join(APP_NAME).with_extension("app");
36+
37+
clean_dir(&app_dir);
38+
39+
create_app(&app_dir, APP_ID, APP_NAME, app_bin, false);
40+
41+
for helper_type in [None, Some("GPU"), Some("Renderer"), Some("Plugin"), Some("Alerts")] {
42+
let helper_id_suffix = helper_type.map(|t| format!(".{t}")).unwrap_or_default();
43+
let helper_id = format!("{APP_ID}.helper{helper_id_suffix}");
44+
let helper_name_suffix = helper_type.map(|t| format!(" ({t})")).unwrap_or_default();
45+
let helper_name = format!("{APP_NAME} Helper{helper_name_suffix}");
46+
let helper_app_dir = app_dir.join(FRAMEWORKS_PATH).join(&helper_name).with_extension("app");
47+
create_app(&helper_app_dir, &helper_id, &helper_name, helper_bin, true);
48+
}
49+
50+
copy_dir(&cef_path().join(FRAMEWORK), &app_dir.join(FRAMEWORKS_PATH).join(FRAMEWORK));
51+
52+
app_dir
53+
}
54+
55+
fn create_app(app_dir: &Path, id: &str, name: &str, bin: &Path, is_helper: bool) {
56+
fs::create_dir_all(app_dir.join(EXEC_PATH)).unwrap();
57+
58+
let app_contents_dir: &Path = &app_dir.join("Contents");
59+
for p in &[EXEC_PATH, RESOURCES_PATH, FRAMEWORKS_PATH] {
60+
fs::create_dir_all(app_contents_dir.join(p)).unwrap();
61+
}
62+
63+
create_info_plist(app_contents_dir, id, name, is_helper).unwrap();
64+
fs::copy(bin, app_dir.join(EXEC_PATH).join(name)).unwrap();
65+
}
66+
67+
fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) -> Result<(), Box<dyn std::error::Error>> {
68+
let info = InfoPlist {
69+
cf_bundle_development_region: "en".to_string(),
70+
cf_bundle_display_name: exec_name.to_string(),
71+
cf_bundle_executable: exec_name.to_string(),
72+
cf_bundle_identifier: id.to_string(),
73+
cf_bundle_info_dictionary_version: "6.0".to_string(),
74+
cf_bundle_name: exec_name.to_string(),
75+
cf_bundle_package_type: "APPL".to_string(),
76+
cf_bundle_signature: "????".to_string(),
77+
cf_bundle_version: "0.0.0".to_string(),
78+
cf_bundle_short_version_string: "0.0".to_string(),
79+
ls_environment: [("MallocNanoZone".to_string(), "0".to_string())].iter().cloned().collect(),
80+
ls_file_quarantine_enabled: true,
81+
ls_minimum_system_version: "11.0".to_string(),
82+
ls_ui_element: if is_helper { Some("1".to_string()) } else { None },
83+
ns_bluetooth_always_usage_description: exec_name.to_string(),
84+
ns_supports_automatic_graphics_switching: true,
85+
ns_web_browser_publickey_credential_usage_description: exec_name.to_string(),
86+
ns_camera_usage_description: exec_name.to_string(),
87+
ns_microphone_usage_description: exec_name.to_string(),
88+
};
89+
90+
let plist_file = dir.join("Info.plist");
91+
plist::to_file_xml(plist_file, &info)?;
92+
Ok(())
93+
}
94+
95+
#[derive(serde::Serialize)]
96+
struct InfoPlist {
97+
#[serde(rename = "CFBundleDevelopmentRegion")]
98+
cf_bundle_development_region: String,
99+
#[serde(rename = "CFBundleDisplayName")]
100+
cf_bundle_display_name: String,
101+
#[serde(rename = "CFBundleExecutable")]
102+
cf_bundle_executable: String,
103+
#[serde(rename = "CFBundleIdentifier")]
104+
cf_bundle_identifier: String,
105+
#[serde(rename = "CFBundleInfoDictionaryVersion")]
106+
cf_bundle_info_dictionary_version: String,
107+
#[serde(rename = "CFBundleName")]
108+
cf_bundle_name: String,
109+
#[serde(rename = "CFBundlePackageType")]
110+
cf_bundle_package_type: String,
111+
#[serde(rename = "CFBundleSignature")]
112+
cf_bundle_signature: String,
113+
#[serde(rename = "CFBundleVersion")]
114+
cf_bundle_version: String,
115+
#[serde(rename = "CFBundleShortVersionString")]
116+
cf_bundle_short_version_string: String,
117+
#[serde(rename = "LSEnvironment")]
118+
ls_environment: HashMap<String, String>,
119+
#[serde(rename = "LSFileQuarantineEnabled")]
120+
ls_file_quarantine_enabled: bool,
121+
#[serde(rename = "LSMinimumSystemVersion")]
122+
ls_minimum_system_version: String,
123+
#[serde(rename = "LSUIElement")]
124+
ls_ui_element: Option<String>,
125+
#[serde(rename = "NSBluetoothAlwaysUsageDescription")]
126+
ns_bluetooth_always_usage_description: String,
127+
#[serde(rename = "NSSupportsAutomaticGraphicsSwitching")]
128+
ns_supports_automatic_graphics_switching: bool,
129+
#[serde(rename = "NSWebBrowserPublicKeyCredentialUsageDescription")]
130+
ns_web_browser_publickey_credential_usage_description: String,
131+
#[serde(rename = "NSCameraUsageDescription")]
132+
ns_camera_usage_description: String,
133+
#[serde(rename = "NSMicrophoneUsageDescription")]
134+
ns_microphone_usage_description: String,
135+
}

desktop/bundle/src/main.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
mod common;
2+
3+
#[cfg(target_os = "linux")]
4+
mod linux;
5+
#[cfg(target_os = "macos")]
6+
mod mac;
7+
#[cfg(target_os = "windows")]
8+
mod win;
9+
10+
fn main() {
11+
#[cfg(target_os = "linux")]
12+
linux::main().unwrap();
13+
#[cfg(target_os = "macos")]
14+
mac::main().unwrap();
15+
#[cfg(target_os = "windows")]
16+
win::main().unwrap();
17+
}

0 commit comments

Comments
 (0)