diff --git a/.cargo/config.toml b/.cargo/config.toml index 4087f77f96..b6ad2fee27 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -10,3 +10,6 @@ rustflags = [ "link-arg=--max-memory=4294967296", "--cfg=web_sys_unstable_apis", ] + +[env] +CARGO_WORKSPACE_DIR = { value = "", relative = true } diff --git a/Cargo.lock b/Cargo.lock index 547ff2fcc3..e2e3166ba1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2285,7 +2285,15 @@ dependencies = [ "wgpu", "windows", "winit", - "winres", +] + +[[package]] +name = "graphite-desktop-bundle" +version = "0.1.0" +dependencies = [ + "cef-dll-sys", + "plist", + "serde", ] [[package]] @@ -2295,6 +2303,21 @@ dependencies = [ "include_dir", ] +[[package]] +name = "graphite-desktop-platform-mac" +version = "0.1.0" +dependencies = [ + "graphite-desktop", +] + +[[package]] +name = "graphite-desktop-platform-win" +version = "0.1.0" +dependencies = [ + "graphite-desktop", + "winres", +] + [[package]] name = "graphite-desktop-wrapper" version = "0.1.0" @@ -4086,6 +4109,19 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "plist" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "740ebea15c5d1428f910cd1a5f52cebf8d25006245ed8ade92702f4943d91e07" +dependencies = [ + "base64", + "indexmap", + "quick-xml 0.38.3", + "serde", + "time", +] + [[package]] name = "plotters" version = "0.3.7" @@ -4304,6 +4340,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "quick-xml" +version = "0.38.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89" +dependencies = [ + "memchr", +] + [[package]] name = "quinn" version = "0.11.9" @@ -6523,7 +6568,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" dependencies = [ "proc-macro2", - "quick-xml", + "quick-xml 0.37.5", "quote", ] diff --git a/Cargo.toml b/Cargo.toml index c8e7bf435e..4f3d445b1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,9 @@ members = [ "desktop", "desktop/wrapper", "desktop/embedded-resources", + "desktop/bundle", + "desktop/platform/mac", + "desktop/platform/win", "editor", "frontend/wasm", "libraries/dyn-any", diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index a3a0bf13fe..a73c3c2b71 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -73,6 +73,3 @@ core-foundation = { version = "0.10", optional = true } # Linux-specific dependencies [target.'cfg(target_os = "linux")'.dependencies] libc = { version = "0.2", optional = true } - -[target.'cfg(target_os = "windows")'.build-dependencies] -winres = "0.1" diff --git a/desktop/bundle/Cargo.toml b/desktop/bundle/Cargo.toml new file mode 100644 index 0000000000..3519a42eb5 --- /dev/null +++ b/desktop/bundle/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "graphite-desktop-bundle" +version = "0.1.0" +description = "Graphite Desktop Bundle" +authors = ["Graphite Authors "] +license = "Apache-2.0" +repository = "" +edition = "2024" +rust-version = "1.87" + +[dependencies] +cef-dll-sys = { workspace = true } + +[target.'cfg(target_os = "macos")'.dependencies] +serde = { workspace = true } +plist = { version = "*" } diff --git a/desktop/bundle/build.rs b/desktop/bundle/build.rs new file mode 100644 index 0000000000..01b6ec29b5 --- /dev/null +++ b/desktop/bundle/build.rs @@ -0,0 +1,10 @@ +fn main() { + println!("cargo:rerun-if-env-changed=CARGO_PROFILE"); + println!("cargo:rerun-if-env-changed=PROFILE"); + let profile = std::env::var("CARGO_PROFILE").or_else(|_| std::env::var("PROFILE")).unwrap(); + println!("cargo:rustc-env=CARGO_PROFILE={profile}"); + + println!("cargo:rerun-if-env-changed=DEP_CEF_DLL_WRAPPER_CEF_DIR"); + let cef_dir = std::env::var("DEP_CEF_DLL_WRAPPER_CEF_DIR").unwrap(); + println!("cargo:rustc-env=CEF_PATH={cef_dir}"); +} diff --git a/desktop/bundle/src/common.rs b/desktop/bundle/src/common.rs new file mode 100644 index 0000000000..2865bf4af3 --- /dev/null +++ b/desktop/bundle/src/common.rs @@ -0,0 +1,66 @@ +use std::error::Error; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; + +pub(crate) const APP_NAME: &str = "Graphite"; + +fn profile_name() -> &'static str { + let mut profile = env!("CARGO_PROFILE"); + if profile == "debug" { + profile = "dev"; + } + profile +} + +pub(crate) fn profile_path() -> PathBuf { + PathBuf::from(env!("CARGO_WORKSPACE_DIR")).join(format!("target/{}", env!("CARGO_PROFILE"))) +} + +pub(crate) fn cef_path() -> PathBuf { + PathBuf::from(env!("CEF_PATH")) +} + +pub(crate) fn build_bin(package: &str, bin: Option<&str>) -> Result> { + let profile = &profile_name(); + let mut args = vec!["build", "--package", package, "--profile", profile]; + if let Some(bin) = bin { + args.push("--bin"); + args.push(bin); + } + run_command("cargo", &args)?; + let profile_path = profile_path(); + let mut bin_path = if let Some(bin) = bin { profile_path.join(bin) } else { profile_path.join(package) }; + if cfg!(target_os = "windows") { + bin_path.set_extension("exe"); + } + Ok(bin_path) +} + +pub(crate) fn run_command(program: &str, args: &[&str]) -> Result<(), Box> { + let status = Command::new(program).args(args).stdout(Stdio::inherit()).stderr(Stdio::inherit()).status()?; + if !status.success() { + std::process::exit(1); + } + Ok(()) +} + +pub(crate) fn clean_dir(dir: &Path) { + if dir.exists() { + fs::remove_dir_all(dir).unwrap(); + } + fs::create_dir_all(dir).unwrap(); +} + +pub(crate) fn copy_dir(src: &Path, dst: &Path) { + fs::create_dir_all(dst).unwrap(); + for entry in fs::read_dir(src).unwrap() { + let entry = entry.unwrap(); + let dst_path = dst.join(entry.file_name()); + if entry.file_type().unwrap().is_dir() { + copy_dir(&entry.path(), &dst_path); + } else { + fs::copy(entry.path(), &dst_path).unwrap(); + } + } +} diff --git a/desktop/bundle/src/mac.rs b/desktop/bundle/src/mac.rs new file mode 100644 index 0000000000..1e1133bf05 --- /dev/null +++ b/desktop/bundle/src/mac.rs @@ -0,0 +1,135 @@ +use std::collections::HashMap; +use std::error::Error; +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::common::*; + +const APP_ID: &str = "rs.graphite.GraphiteEditor"; + +const PACKAGE: &str = "graphite-desktop-platform-mac"; +const HELPER_BIN: &str = "graphite-desktop-platform-mac-helper"; + +const EXEC_PATH: &str = "Contents/MacOS"; +const FRAMEWORKS_PATH: &str = "Contents/Frameworks"; +const RESOURCES_PATH: &str = "Contents/Resources"; +const FRAMEWORK: &str = "Chromium Embedded Framework.framework"; + +pub fn main() -> Result<(), Box> { + let app_bin = build_bin(PACKAGE, None)?; + let helper_bin = build_bin(PACKAGE, Some(HELPER_BIN))?; + + let profile_path = profile_path(); + let app_dir = bundle(&profile_path, &app_bin, &helper_bin); + + // TODO: Consider adding more useful cli + if std::env::args().any(|a| a == "open") { + let app_path = app_dir.to_string_lossy(); + run_command("open", &[&app_path]).expect("failed to open app") + } + + Ok(()) +} + +fn bundle(out_dir: &Path, app_bin: &Path, helper_bin: &Path) -> PathBuf { + let app_dir = out_dir.join(APP_NAME).with_extension("app"); + + clean_dir(&app_dir); + + create_app(&app_dir, APP_ID, APP_NAME, app_bin, false); + + for helper_type in [None, Some("GPU"), Some("Renderer"), Some("Plugin"), Some("Alerts")] { + let helper_id_suffix = helper_type.map(|t| format!(".{t}")).unwrap_or_default(); + let helper_id = format!("{APP_ID}.helper{helper_id_suffix}"); + let helper_name_suffix = helper_type.map(|t| format!(" ({t})")).unwrap_or_default(); + let helper_name = format!("{APP_NAME} Helper{helper_name_suffix}"); + let helper_app_dir = app_dir.join(FRAMEWORKS_PATH).join(&helper_name).with_extension("app"); + create_app(&helper_app_dir, &helper_id, &helper_name, helper_bin, true); + } + + copy_dir(&cef_path().join(FRAMEWORK), &app_dir.join(FRAMEWORKS_PATH).join(FRAMEWORK)); + + app_dir +} + +fn create_app(app_dir: &Path, id: &str, name: &str, bin: &Path, is_helper: bool) { + fs::create_dir_all(app_dir.join(EXEC_PATH)).unwrap(); + + let app_contents_dir: &Path = &app_dir.join("Contents"); + for p in &[EXEC_PATH, RESOURCES_PATH, FRAMEWORKS_PATH] { + fs::create_dir_all(app_contents_dir.join(p)).unwrap(); + } + + create_info_plist(app_contents_dir, id, name, is_helper).unwrap(); + fs::copy(bin, app_dir.join(EXEC_PATH).join(name)).unwrap(); +} + +fn create_info_plist(dir: &Path, id: &str, exec_name: &str, is_helper: bool) -> Result<(), Box> { + let info = InfoPlist { + cf_bundle_development_region: "en".to_string(), + cf_bundle_display_name: exec_name.to_string(), + cf_bundle_executable: exec_name.to_string(), + cf_bundle_identifier: id.to_string(), + cf_bundle_info_dictionary_version: "6.0".to_string(), + cf_bundle_name: exec_name.to_string(), + cf_bundle_package_type: "APPL".to_string(), + cf_bundle_signature: "????".to_string(), + cf_bundle_version: "0.0.0".to_string(), + cf_bundle_short_version_string: "0.0".to_string(), + ls_environment: [("MallocNanoZone".to_string(), "0".to_string())].iter().cloned().collect(), + ls_file_quarantine_enabled: true, + ls_minimum_system_version: "11.0".to_string(), + ls_ui_element: if is_helper { Some("1".to_string()) } else { None }, + ns_bluetooth_always_usage_description: exec_name.to_string(), + ns_supports_automatic_graphics_switching: true, + ns_web_browser_publickey_credential_usage_description: exec_name.to_string(), + ns_camera_usage_description: exec_name.to_string(), + ns_microphone_usage_description: exec_name.to_string(), + }; + + let plist_file = dir.join("Info.plist"); + plist::to_file_xml(plist_file, &info)?; + Ok(()) +} + +#[derive(serde::Serialize)] +struct InfoPlist { + #[serde(rename = "CFBundleDevelopmentRegion")] + cf_bundle_development_region: String, + #[serde(rename = "CFBundleDisplayName")] + cf_bundle_display_name: String, + #[serde(rename = "CFBundleExecutable")] + cf_bundle_executable: String, + #[serde(rename = "CFBundleIdentifier")] + cf_bundle_identifier: String, + #[serde(rename = "CFBundleInfoDictionaryVersion")] + cf_bundle_info_dictionary_version: String, + #[serde(rename = "CFBundleName")] + cf_bundle_name: String, + #[serde(rename = "CFBundlePackageType")] + cf_bundle_package_type: String, + #[serde(rename = "CFBundleSignature")] + cf_bundle_signature: String, + #[serde(rename = "CFBundleVersion")] + cf_bundle_version: String, + #[serde(rename = "CFBundleShortVersionString")] + cf_bundle_short_version_string: String, + #[serde(rename = "LSEnvironment")] + ls_environment: HashMap, + #[serde(rename = "LSFileQuarantineEnabled")] + ls_file_quarantine_enabled: bool, + #[serde(rename = "LSMinimumSystemVersion")] + ls_minimum_system_version: String, + #[serde(rename = "LSUIElement")] + ls_ui_element: Option, + #[serde(rename = "NSBluetoothAlwaysUsageDescription")] + ns_bluetooth_always_usage_description: String, + #[serde(rename = "NSSupportsAutomaticGraphicsSwitching")] + ns_supports_automatic_graphics_switching: bool, + #[serde(rename = "NSWebBrowserPublicKeyCredentialUsageDescription")] + ns_web_browser_publickey_credential_usage_description: String, + #[serde(rename = "NSCameraUsageDescription")] + ns_camera_usage_description: String, + #[serde(rename = "NSMicrophoneUsageDescription")] + ns_microphone_usage_description: String, +} diff --git a/desktop/bundle/src/main.rs b/desktop/bundle/src/main.rs new file mode 100644 index 0000000000..e06d3456dd --- /dev/null +++ b/desktop/bundle/src/main.rs @@ -0,0 +1,15 @@ +mod common; + +#[cfg(target_os = "macos")] +mod mac; +#[cfg(target_os = "windows")] +mod win; + +fn main() { + #[cfg(target_os = "macos")] + mac::main().unwrap(); + #[cfg(target_os = "windows")] + win::main().unwrap(); + #[cfg(target_os = "linux")] + todo!("Linux bundling not implemented yet"); +} diff --git a/desktop/bundle/src/win.rs b/desktop/bundle/src/win.rs new file mode 100644 index 0000000000..d07c3ef9bb --- /dev/null +++ b/desktop/bundle/src/win.rs @@ -0,0 +1,35 @@ +use std::error::Error; +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::common::*; + +const PACKAGE: &str = "graphite-desktop-platform-win"; +const EXECUTABLE: &str = "graphite-editor.exe"; + +pub fn main() -> Result<(), Box> { + let app_bin = build_bin(PACKAGE, None)?; + + let executable = bundle(&profile_path(), &app_bin); + + // TODO: Consider adding more useful cli + if std::env::args().any(|a| a == "open") { + let executable_path = executable.to_string_lossy(); + run_command(&executable_path, &[]).expect("failed to open app") + } + + Ok(()) +} + +fn bundle(out_dir: &Path, app_bin: &Path) -> PathBuf { + let app_dir = out_dir.join(APP_NAME); + + clean_dir(&app_dir); + + copy_dir(&cef_path(), &app_dir); + + let bin_path = app_dir.join(EXECUTABLE); + fs::copy(app_bin, &bin_path).unwrap(); + + bin_path +} diff --git a/desktop/platform/mac/Cargo.toml b/desktop/platform/mac/Cargo.toml new file mode 100644 index 0000000000..5b504d62a1 --- /dev/null +++ b/desktop/platform/mac/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "graphite-desktop-platform-mac" +version = "0.1.0" +description = "Graphite Desktop Platform Mac" +authors = ["Graphite Authors "] +license = "Apache-2.0" +repository = "" +edition = "2024" +rust-version = "1.87" + +[[bin]] +name = "graphite-desktop-platform-mac-helper" +path = "src/helper.rs" + +[dependencies] +graphite-desktop = { path = "../.." } diff --git a/desktop/platform/mac/src/helper.rs b/desktop/platform/mac/src/helper.rs new file mode 100644 index 0000000000..149bb79e84 --- /dev/null +++ b/desktop/platform/mac/src/helper.rs @@ -0,0 +1,3 @@ +fn main() { + graphite_desktop::start_helper(); +} diff --git a/desktop/platform/mac/src/main.rs b/desktop/platform/mac/src/main.rs new file mode 100644 index 0000000000..5420d97bbe --- /dev/null +++ b/desktop/platform/mac/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + graphite_desktop::start(); +} diff --git a/desktop/platform/win/Cargo.toml b/desktop/platform/win/Cargo.toml new file mode 100644 index 0000000000..293ff0e8db --- /dev/null +++ b/desktop/platform/win/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "graphite-desktop-platform-win" +version = "0.1.0" +description = "Graphite Desktop Platform Windows" +authors = ["Graphite Authors "] +license = "Apache-2.0" +repository = "" +edition = "2024" +rust-version = "1.87" + +[dependencies] +graphite-desktop = { path = "../.." } + +[target.'cfg(target_os = "windows")'.build-dependencies] +winres = "0.1" diff --git a/desktop/build.rs b/desktop/platform/win/build.rs similarity index 74% rename from desktop/build.rs rename to desktop/platform/win/build.rs index bf70ac6202..d52ffbedb7 100644 --- a/desktop/build.rs +++ b/desktop/platform/win/build.rs @@ -2,7 +2,7 @@ fn main() { #[cfg(target_os = "windows")] { let mut res = winres::WindowsResource::new(); - res.set_icon("assets/graphite-icon-color.ico"); + res.set_icon("../../assets/graphite-icon-color.ico"); res.compile().expect("Failed to compile Windows resources"); } } diff --git a/desktop/platform/win/src/main.rs b/desktop/platform/win/src/main.rs new file mode 100644 index 0000000000..2864480316 --- /dev/null +++ b/desktop/platform/win/src/main.rs @@ -0,0 +1,4 @@ +#![windows_subsystem = "windows"] +fn main() { + graphite_desktop::start(); +}

AltStyle によって変換されたページ (->オリジナル) /