mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-15 22:28:10 +08:00
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
This commit is contained in:
66
desktop/bundle/src/common.rs
Normal file
66
desktop/bundle/src/common.rs
Normal file
@@ -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<PathBuf, Box<dyn Error>> {
|
||||
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<dyn std::error::Error>> {
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
23
desktop/bundle/src/linux.rs
Normal file
23
desktop/bundle/src/linux.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use std::error::Error;
|
||||
|
||||
use crate::common::*;
|
||||
|
||||
const PACKAGE: &str = "graphite-desktop-platform-linux";
|
||||
|
||||
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||
let app_bin = build_bin(PACKAGE, None)?;
|
||||
|
||||
// TODO: Implement bundling for linux
|
||||
|
||||
// TODO: Consider adding more useful cli
|
||||
if std::env::args().any(|a| a == "open") {
|
||||
run_command(&app_bin.to_string_lossy(), &[]).expect("failed to open app");
|
||||
} else {
|
||||
println!("Binary built and placed at {}", app_bin.to_string_lossy());
|
||||
eprintln!("Bundling for Linux is not yet implemented.");
|
||||
eprintln!("You can still start the app with the `open` subcommand. `cargo run -p graphite-desktop-bundle -- open`");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
135
desktop/bundle/src/mac.rs
Normal file
135
desktop/bundle/src/mac.rs
Normal file
@@ -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<dyn Error>> {
|
||||
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 executable_path = app_dir.join(EXEC_PATH).join(APP_NAME);
|
||||
run_command(&executable_path.to_string_lossy(), &[]).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<dyn std::error::Error>> {
|
||||
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<String, String>,
|
||||
#[serde(rename = "LSFileQuarantineEnabled")]
|
||||
ls_file_quarantine_enabled: bool,
|
||||
#[serde(rename = "LSMinimumSystemVersion")]
|
||||
ls_minimum_system_version: String,
|
||||
#[serde(rename = "LSUIElement")]
|
||||
ls_ui_element: Option<String>,
|
||||
#[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,
|
||||
}
|
||||
17
desktop/bundle/src/main.rs
Normal file
17
desktop/bundle/src/main.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
mod common;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod linux;
|
||||
#[cfg(target_os = "macos")]
|
||||
mod mac;
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win;
|
||||
|
||||
fn main() {
|
||||
#[cfg(target_os = "linux")]
|
||||
linux::main().unwrap();
|
||||
#[cfg(target_os = "macos")]
|
||||
mac::main().unwrap();
|
||||
#[cfg(target_os = "windows")]
|
||||
win::main().unwrap();
|
||||
}
|
||||
35
desktop/bundle/src/win.rs
Normal file
35
desktop/bundle/src/win.rs
Normal file
@@ -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<dyn Error>> {
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user