diff --git a/Cargo.lock b/Cargo.lock index 6ba448b8be..4d7c59d660 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -411,6 +411,18 @@ dependencies = [ "libbz2-rs-sys", ] +[[package]] +name = "cab" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171228650e6721d5acc0868a462cd864f49ac5f64e4a42cde270406e64e404d2" +dependencies = [ + "byteorder", + "flate2", + "lzxd", + "time", +] + [[package]] name = "calloop" version = "0.14.3" @@ -593,6 +605,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" +[[package]] +name = "cfb" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3" +dependencies = [ + "fnv", + "uuid", + "web-time", +] + [[package]] name = "cfg-if" version = "1.0.3" @@ -2107,7 +2130,9 @@ dependencies = [ name = "graphite-desktop-bundle" version = "0.0.0" dependencies = [ + "cab", "cef-dll-sys", + "msi", "plist", "serde", ] @@ -2138,8 +2163,6 @@ name = "graphite-desktop-platform-win" version = "0.0.0" dependencies = [ "graphite-desktop", - "windows 0.62.2", - "windows-registry 0.6.1", "winres", ] @@ -2441,7 +2464,7 @@ dependencies = [ "tokio", "tower-service", "tracing", - "windows-registry 0.5.3", + "windows-registry", ] [[package]] @@ -3023,6 +3046,12 @@ dependencies = [ "sha2", ] +[[package]] +name = "lzxd" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9" + [[package]] name = "mac" version = "0.1.1" @@ -3151,6 +3180,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "msi" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0325f8473ef1f5c38ee42345e2cd1678299cbbfa169d1776654a2a682867420" +dependencies = [ + "byteorder", + "cfb", + "encoding_rs", + "uuid", +] + [[package]] name = "muda" version = "0.17.1" @@ -6069,6 +6110,16 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "valuable" version = "0.1.1" @@ -6850,17 +6901,6 @@ dependencies = [ "windows-strings 0.4.2", ] -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - [[package]] name = "windows-result" version = "0.2.0" diff --git a/desktop/bundle/Cargo.toml b/desktop/bundle/Cargo.toml index acaaa2ec49..180d489df9 100644 --- a/desktop/bundle/Cargo.toml +++ b/desktop/bundle/Cargo.toml @@ -14,3 +14,7 @@ cef-dll-sys = { workspace = true } [target.'cfg(target_os = "macos")'.dependencies] serde = { workspace = true } plist = { version = "*" } + +[target.'cfg(target_os = "windows")'.dependencies] +msi = "0.10" +cab = "0.6" diff --git a/desktop/bundle/src/win.rs b/desktop/bundle/src/win.rs index f673124cf8..cf55c7fae6 100644 --- a/desktop/bundle/src/win.rs +++ b/desktop/bundle/src/win.rs @@ -1,61 +1,574 @@ +use std::collections::{BTreeMap, BTreeSet}; use std::error::Error; use std::fs; +use std::io::{Cursor, Write}; use std::path::{Path, PathBuf}; +use cab::{CabinetBuilder, CompressionType}; +use msi::{Column, Insert, Package, PackageType, Value}; + use crate::common::*; const EXECUTABLE: &str = "Graphite.exe"; +const PRODUCT_VERSION: &str = "0.0.0"; +const MANUFACTURER: &str = "Graphite Labs, LLC"; +const UPGRADE_CODE: &str = "{8D42B6D4-B0E8-41BC-9F41-74F1E319BC1D}"; + +const PROG_ID: &str = "Graphite.Document"; +const DOCUMENT_FRIENDLY_NAME: &str = "Graphite Document"; +const FILE_EXTENSION: &str = ".graphite"; +const MIME_TYPE: &str = "application/graphite+json"; +const EXTRA_EXTENSIONS: &[&str] = &[".svg", ".png", ".jpg", ".jpeg"]; + +const CABINET_STREAM: &str = "Data1.cab"; pub fn main() -> Result<(), Box> { let app_bin = build_bin("graphite-desktop-platform-win", None)?; - - let executable = bundle(&profile_path(), &app_bin); + let profile_path = profile_path(); + let bundle_dir = bundle(&profile_path, &app_bin); + let msi_path = create_msi(&bundle_dir, &profile_path)?; // TODO: Consider adding more useful cli let args: Vec = std::env::args().collect(); if let Some(pos) = args.iter().position(|a| a == "open") { + let executable = bundle_dir.join(EXECUTABLE); let extra_args: Vec<&str> = args[pos + 1..].iter().map(|s| s.as_str()).collect(); - run_command(&executable.to_string_lossy(), &extra_args).expect("failed to open app") + run_command(&executable.to_string_lossy(), &extra_args)?; + } else { + println!("MSI created at: {}", msi_path.display()); } - 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); - if let Err(e) = remove_unnecessary_cef_files(&app_dir) { eprintln!("Failed to remove unnecessary CEF files: {}", e); } - - let bin_path = app_dir.join(EXECUTABLE); - fs::copy(app_bin, &bin_path).unwrap(); - - bin_path + fs::copy(app_bin, app_dir.join(EXECUTABLE)).unwrap(); + app_dir } fn remove_unnecessary_cef_files(app_dir: &Path) -> Result<(), Box> { fs::remove_dir_all(app_dir.join("cmake"))?; fs::remove_dir_all(app_dir.join("include"))?; fs::remove_dir_all(app_dir.join("libcef_dll"))?; - for entry in fs::read_dir(app_dir.join("locales"))? { let path = entry?.path(); if path.is_file() && path.file_name() != Some("en-US.pak".as_ref()) { fs::remove_file(path)?; } } - fs::remove_file(app_dir.join("archive.json"))?; fs::remove_file(app_dir.join("CMakeLists.txt"))?; fs::remove_file(app_dir.join("bootstrapc.exe"))?; fs::remove_file(app_dir.join("bootstrap.exe"))?; fs::remove_file(app_dir.join("libcef.lib"))?; fs::remove_file(app_dir.join("CREDITS.html"))?; - Ok(()) } + +struct AppFile { + abs_path: PathBuf, + file_id: String, + msi_filename: String, + component_id: String, + sequence: u32, + size: u64, +} + +struct AppDirectory { + dir_id: String, + parent_id: Option, + default_dir: String, +} + +struct AppComponent { + component_id: String, + component_guid: String, + dir_id: String, + key_file_id: String, +} + +struct Entries { + files: Vec, + directories: Vec, + components: Vec, +} + +fn product_code() -> String { + let commit = std::process::Command::new("git") + .args(["rev-parse", "HEAD"]) + .output() + .ok() + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_ascii_uppercase()) + .filter(|s| s.len() >= 32) + .unwrap_or_else(|| "0".repeat(32)); + format!("{{{}-{}-{}-{}-{}}}", &commit[..8], &commit[8..12], &commit[12..16], &commit[16..20], &commit[20..32],) +} + +fn fnv1a(s: &str) -> u64 { + s.bytes().fold(14695981039346656037u64, |h, b| (h ^ b as u64).wrapping_mul(1099511628211)) +} + +fn component_guid(seed: &str) -> String { + let h1 = fnv1a(seed); + let h2 = fnv1a(&format!("{seed}_2")); + format!( + "{{{:08X}-{:04X}-{:04X}-{:04X}-{:012X}}}", + (h1 >> 32) as u32, + ((h1 >> 16) & 0xFFFF) as u16, + (h1 & 0xFFFF) as u16, + ((h2 >> 48) & 0xFFFF) as u16, + h2 & 0x0000_FFFF_FFFF_FFFF, + ) +} + +fn msi_filename(name: &str) -> String { + let dot = name.rfind('.'); + let (base, ext) = dot.map(|i| (&name[..i], &name[i + 1..])).unwrap_or((name, "")); + if base.len() <= 8 && ext.len() <= 3 { + return name.to_string(); + } + let short_base: String = base.chars().filter(|c| c.is_ascii_alphanumeric()).take(6).map(|c| c.to_ascii_uppercase()).collect(); + let short_ext: String = ext.chars().filter(|c| c.is_ascii_alphanumeric()).take(3).map(|c| c.to_ascii_uppercase()).collect(); + if short_ext.is_empty() { + format!("{short_base}~1|{name}") + } else { + format!("{short_base}~1.{short_ext}|{name}") + } +} + +fn collect_entries(bundle_dir: &Path) -> Result> { + let mut raw_files: Vec<(PathBuf, PathBuf)> = Vec::new(); + gather_files(bundle_dir, bundle_dir, &mut raw_files)?; + + let mut all_dir_strs: BTreeSet = BTreeSet::new(); + all_dir_strs.insert(String::new()); + for (_, rel) in &raw_files { + let mut path = rel.parent().unwrap_or(Path::new("")).to_owned(); + loop { + let s = path.to_string_lossy().replace('\\', "/"); + if s.is_empty() { + break; + } + all_dir_strs.insert(s); + match path.parent().filter(|p| !p.as_os_str().is_empty()) { + Some(p) => path = p.to_owned(), + None => break, + } + } + } + + let mut dir_to_ids: BTreeMap = BTreeMap::new(); + for dir_str in &all_dir_strs { + let h = fnv1a(dir_str); + let dir_id = if dir_str.is_empty() { "INSTALLDIR".to_string() } else { format!("DIR_{h:016X}") }; + let comp_id = format!("COMP_{h:016X}"); + dir_to_ids.insert(dir_str.clone(), (dir_id, comp_id)); + } + + let mut directories = vec![ + AppDirectory { + dir_id: "TARGETDIR".into(), + parent_id: None, + default_dir: "SourceDir".into(), + }, + AppDirectory { + dir_id: "ProgramFiles64Folder".into(), + parent_id: Some("TARGETDIR".into()), + default_dir: ".".into(), + }, + AppDirectory { + dir_id: "INSTALLDIR".into(), + parent_id: Some("ProgramFiles64Folder".into()), + default_dir: APP_NAME.into(), + }, + ]; + let mut subdir_strs: Vec<&str> = all_dir_strs.iter().filter(|s| !s.is_empty()).map(String::as_str).collect(); + subdir_strs.sort_by_key(|s| s.len()); + for dir_str in subdir_strs { + let (dir_id, _) = &dir_to_ids[dir_str]; + let path = Path::new(dir_str); + let parent_str = path.parent().filter(|p| !p.as_os_str().is_empty()).map(|p| p.to_string_lossy().replace('\\', "/")).unwrap_or_default(); + let parent_id = dir_to_ids.get(parent_str.as_str()).map(|(id, _)| id.clone()).unwrap_or_else(|| "INSTALLDIR".into()); + let name = path.file_name().unwrap_or_default().to_string_lossy().into_owned(); + directories.push(AppDirectory { + dir_id: dir_id.clone(), + parent_id: Some(parent_id), + default_dir: name, + }); + } + + let mut comp_key_files: BTreeMap = BTreeMap::new(); + let mut file_dir_strs: BTreeSet = BTreeSet::new(); + let mut files = Vec::new(); + let mut seq = 1u32; + + for (abs_path, rel_path) in &raw_files { + let dir_str = rel_path.parent().unwrap_or(Path::new("")).to_string_lossy().replace('\\', "/"); + let (dir_id, comp_id) = dir_to_ids[dir_str.as_str()].clone(); + file_dir_strs.insert(dir_str.clone()); + + let h = fnv1a(&rel_path.to_string_lossy().replace('\\', "/")); + let file_id = format!("FILE_{h:016X}"); + let filename = msi_filename(rel_path.file_name().unwrap_or_default().to_str().unwrap_or_default()); + let size = fs::metadata(abs_path)?.len(); + + comp_key_files.entry(comp_id.clone()).or_insert_with(|| file_id.clone()); + files.push(AppFile { + abs_path: abs_path.clone(), + file_id, + msi_filename: filename, + component_id: comp_id, + sequence: seq, + size, + }); + seq += 1; + } + + let components: Vec = file_dir_strs + .iter() + .filter_map(|dir_str| { + let (dir_id, comp_id) = dir_to_ids.get(dir_str.as_str())?; + let key_file_id = comp_key_files.get(comp_id)?.clone(); + Some(AppComponent { + component_id: comp_id.clone(), + component_guid: component_guid(dir_str), + dir_id: dir_id.clone(), + key_file_id, + }) + }) + .collect(); + + Ok(Entries { files, directories, components }) +} + +fn gather_files(base: &Path, dir: &Path, out: &mut Vec<(PathBuf, PathBuf)>) -> std::io::Result<()> { + let mut entries: Vec<_> = fs::read_dir(dir)?.collect::>()?; + entries.sort_by_key(|e| e.file_name()); + for entry in entries { + let path = entry.path(); + if path.is_dir() { + gather_files(base, &path, out)?; + } else { + let rel = path.strip_prefix(base).unwrap().to_path_buf(); + out.push((path, rel)); + } + } + Ok(()) +} + +fn build_cabinet(files: &[AppFile]) -> Result, Box> { + let mut builder = CabinetBuilder::new(); + { + let folder = builder.add_folder(CompressionType::MSZIP); + for file in files { + folder.add_file(file.file_id.as_str()); + } + } + let mut writer = builder.build(Cursor::new(Vec::new()))?; + let mut iter = files.iter(); + while let Some(mut fw) = writer.next_file()? { + let file = iter.next().expect("cabinet/file count mismatch"); + fw.write_all(&fs::read(&file.abs_path)?)?; + } + Ok(writer.finish()?.into_inner()) +} + +fn create_msi(bundle_dir: &Path, out_dir: &Path) -> Result> { + let entries = collect_entries(bundle_dir)?; + let cabinet_bytes = build_cabinet(&entries.files)?; + + let msi_path = out_dir.join("Graphite.msi"); + let mut pkg = Package::create(PackageType::Installer, fs::File::create(&msi_path)?)?; + + { + let info = pkg.summary_info_mut(); + info.set_title("Graphite Installation Database"); + info.set_subject(APP_NAME); + info.set_author(MANUFACTURER); + info.set_creating_application("graphite-desktop-bundle"); + info.set_arch("x64"); + } + + define_tables(&mut pkg)?; + populate_properties(&mut pkg)?; + populate_directories(&mut pkg, &entries.directories)?; + populate_components(&mut pkg, &entries.components)?; + populate_features(&mut pkg)?; + populate_feature_components(&mut pkg, &entries.components)?; + populate_files(&mut pkg, &entries.files)?; + populate_media(&mut pkg, entries.files.len() as u32)?; + populate_registry(&mut pkg, &entries.components)?; + populate_sequences(&mut pkg)?; + + { + let mut stream = pkg.write_stream(CABINET_STREAM)?; + stream.write_all(&cabinet_bytes)?; + } + + pkg.flush()?; + Ok(msi_path) +} + +fn define_tables(pkg: &mut Package) -> Result<(), Box> { + pkg.create_table("Property", vec![Column::build("Property").primary_key().id_string(72), Column::build("Value").text_string(0)])?; + pkg.create_table( + "Directory", + vec![ + Column::build("Directory").primary_key().id_string(72), + Column::build("Directory_Parent").nullable().id_string(72), + Column::build("DefaultDir").string(255), + ], + )?; + pkg.create_table( + "Component", + vec![ + Column::build("Component").primary_key().id_string(72), + Column::build("ComponentId").nullable().string(38), + Column::build("Directory_").id_string(72), + Column::build("Attributes").int16(), + Column::build("Condition").nullable().text_string(255), + Column::build("KeyPath").nullable().id_string(72), + ], + )?; + pkg.create_table( + "Feature", + vec![ + Column::build("Feature").primary_key().id_string(38), + Column::build("Feature_Parent").nullable().id_string(38), + Column::build("Title").nullable().text_string(64), + Column::build("Description").nullable().text_string(255), + Column::build("Display").nullable().int16(), + Column::build("Level").int16(), + Column::build("Directory_").nullable().id_string(72), + Column::build("Attributes").int16(), + ], + )?; + pkg.create_table( + "FeatureComponents", + vec![Column::build("Feature_").primary_key().id_string(38), Column::build("Component_").primary_key().id_string(72)], + )?; + pkg.create_table( + "File", + vec![ + Column::build("File").primary_key().id_string(72), + Column::build("Component_").id_string(72), + Column::build("FileName").string(255), + Column::build("FileSize").int32(), + Column::build("Version").nullable().string(72), + Column::build("Language").nullable().string(20), + Column::build("Attributes").nullable().int16(), + Column::build("Sequence").int32(), + ], + )?; + pkg.create_table( + "Media", + vec![ + Column::build("DiskId").primary_key().int16(), + Column::build("LastSequence").int32(), + Column::build("DiskPrompt").nullable().text_string(64), + Column::build("Cabinet").nullable().string(255), + Column::build("VolumeLabel").nullable().string(32), + Column::build("Source").nullable().string(72), + ], + )?; + pkg.create_table( + "Registry", + vec![ + Column::build("Registry").primary_key().id_string(72), + Column::build("Root").int16(), + Column::build("Key").formatted_string(255), + Column::build("Name").nullable().formatted_string(255), + Column::build("Value").nullable().formatted_string(0), + Column::build("Component_").id_string(72), + ], + )?; + pkg.create_table( + "InstallExecuteSequence", + vec![ + Column::build("Action").primary_key().id_string(72), + Column::build("Condition").nullable().text_string(255), + Column::build("Sequence").int16(), + ], + )?; + pkg.create_table( + "InstallUISequence", + vec![ + Column::build("Action").primary_key().id_string(72), + Column::build("Condition").nullable().text_string(255), + Column::build("Sequence").int16(), + ], + )?; + Ok(()) +} + +fn populate_properties(pkg: &mut Package) -> Result<(), Box> { + pkg.insert_rows(Insert::into("Property").rows(vec![ + vec![Value::from("ProductName"), Value::from(APP_NAME)], + vec![Value::from("ProductCode"), Value::from(product_code())], + vec![Value::from("ProductVersion"), Value::from(PRODUCT_VERSION)], + vec![Value::from("Manufacturer"), Value::from(MANUFACTURER)], + vec![Value::from("UpgradeCode"), Value::from(UPGRADE_CODE)], + vec![Value::from("INSTALLLEVEL"), Value::from("1")], + ]))?; + Ok(()) +} + +fn populate_directories(pkg: &mut Package, dirs: &[AppDirectory]) -> Result<(), Box> { + let rows: Vec> = dirs + .iter() + .map(|d| { + vec![ + Value::from(d.dir_id.as_str()), + d.parent_id.as_deref().map(Value::from).unwrap_or(Value::Null), + Value::from(d.default_dir.as_str()), + ] + }) + .collect(); + pkg.insert_rows(Insert::into("Directory").rows(rows))?; + Ok(()) +} + +fn populate_components(pkg: &mut Package, components: &[AppComponent]) -> Result<(), Box> { + let rows: Vec> = components + .iter() + .map(|c| { + vec![ + Value::from(c.component_id.as_str()), + Value::from(c.component_guid.as_str()), + Value::from(c.dir_id.as_str()), + Value::from(0i16), + Value::Null, + Value::from(c.key_file_id.as_str()), + ] + }) + .collect(); + pkg.insert_rows(Insert::into("Component").rows(rows))?; + Ok(()) +} + +fn populate_features(pkg: &mut Package) -> Result<(), Box> { + pkg.insert_rows(Insert::into("Feature").row(vec![ + Value::from("ProductFeature"), + Value::Null, + Value::from(APP_NAME), + Value::from("Graphite Vector Editor"), + Value::from(1i16), + Value::from(1i16), + Value::from("INSTALLDIR"), + Value::from(0i16), + ]))?; + Ok(()) +} + +fn populate_feature_components(pkg: &mut Package, components: &[AppComponent]) -> Result<(), Box> { + let rows: Vec> = components.iter().map(|c| vec![Value::from("ProductFeature"), Value::from(c.component_id.as_str())]).collect(); + pkg.insert_rows(Insert::into("FeatureComponents").rows(rows))?; + Ok(()) +} + +fn populate_files(pkg: &mut Package, files: &[AppFile]) -> Result<(), Box> { + let rows: Vec> = files + .iter() + .map(|f| { + vec![ + Value::from(f.file_id.as_str()), + Value::from(f.component_id.as_str()), + Value::from(f.msi_filename.as_str()), + Value::Int(f.size as i32), + Value::Null, + Value::Null, + Value::Null, + Value::Int(f.sequence as i32), + ] + }) + .collect(); + pkg.insert_rows(Insert::into("File").rows(rows))?; + Ok(()) +} + +fn populate_media(pkg: &mut Package, last_sequence: u32) -> Result<(), Box> { + pkg.insert_rows(Insert::into("Media").row(vec![ + Value::from(1i16), + Value::Int(last_sequence as i32), + Value::Null, + Value::from(format!("#{CABINET_STREAM}")), + Value::Null, + Value::Null, + ]))?; + Ok(()) +} + +fn populate_registry(pkg: &mut Package, components: &[AppComponent]) -> Result<(), Box> { + let root_comp = components + .iter() + .find(|c| c.dir_id == "INSTALLDIR") + .map(|c| c.component_id.as_str()) + .unwrap_or_else(|| components.first().map(|c| c.component_id.as_str()).unwrap_or("")); + + let exe = format!("[INSTALLDIR]{EXECUTABLE}"); + let open_cmd = format!("\"[INSTALLDIR]{EXECUTABLE}\" \"%1\""); + + let mut rows: Vec> = vec![ + reg("reg_progid", PROG_ID, None, Some(DOCUMENT_FRIENDLY_NAME), root_comp), + reg("reg_progid_icon", &format!("{PROG_ID}\\DefaultIcon"), None, Some(&format!("{exe},0")), root_comp), + reg("reg_progid_cmd", &format!("{PROG_ID}\\shell\\open\\command"), None, Some(&open_cmd), root_comp), + reg("reg_ext", FILE_EXTENSION, None, Some(PROG_ID), root_comp), + reg("reg_ext_mime", FILE_EXTENSION, Some("Content Type"), Some(MIME_TYPE), root_comp), + reg("reg_app_name", &format!("Applications\\{EXECUTABLE}"), Some("FriendlyAppName"), Some(APP_NAME), root_comp), + reg("reg_app_cmd", &format!("Applications\\{EXECUTABLE}\\shell\\open\\command"), None, Some(&open_cmd), root_comp), + reg("reg_app_ext", &format!("Applications\\{EXECUTABLE}\\SupportedTypes"), Some(FILE_EXTENSION), Some(""), root_comp), + ]; + + for (i, ext) in EXTRA_EXTENSIONS.iter().enumerate() { + rows.push(reg(&format!("reg_app_ext_{i}"), &format!("Applications\\{EXECUTABLE}\\SupportedTypes"), Some(ext), Some(""), root_comp)); + } + + pkg.insert_rows(Insert::into("Registry").rows(rows))?; + Ok(()) +} + +fn reg(id: &str, key: &str, name: Option<&str>, value: Option<&str>, comp: &str) -> Vec { + vec![ + Value::from(id), + Value::from(0i16), + Value::from(key), + name.map(Value::from).unwrap_or(Value::Null), + value.map(Value::from).unwrap_or(Value::Null), + Value::from(comp), + ] +} + +fn populate_sequences(pkg: &mut Package) -> Result<(), Box> { + pkg.insert_rows(Insert::into("InstallExecuteSequence").rows(vec![ + seq("CostInitialize", 800), + seq("FileCost", 900), + seq("CostFinalize", 1000), + seq("InstallValidate", 1400), + seq("InstallInitialize", 1500), + seq("ProcessComponents", 1600), + seq("UnpublishComponents", 1700), + seq("UnpublishFeatures", 1800), + seq("RemoveRegistryValues", 2600), + seq("RemoveFiles", 3500), + seq("InstallFiles", 4000), + seq("WriteRegistryValues", 5000), + seq("RegisterUser", 6000), + seq("RegisterProduct", 6100), + seq("PublishComponents", 6200), + seq("PublishFeatures", 6300), + seq("PublishProduct", 6400), + seq("InstallFinalize", 6600), + ]))?; + pkg.insert_rows(Insert::into("InstallUISequence").rows(vec![seq("CostInitialize", 800), seq("FileCost", 900), seq("CostFinalize", 1000), seq("ExecuteAction", 1300)]))?; + Ok(()) +} + +fn seq(action: &str, sequence: i16) -> Vec { + vec![Value::from(action), Value::Null, Value::from(sequence)] +} diff --git a/desktop/platform/win/Cargo.toml b/desktop/platform/win/Cargo.toml index de23c8e774..e268622cca 100644 --- a/desktop/platform/win/Cargo.toml +++ b/desktop/platform/win/Cargo.toml @@ -15,9 +15,5 @@ path = "src/main.rs" [dependencies] graphite-desktop = { path = "../.." } -[target.'cfg(target_os = "windows")'.dependencies] -windows-registry = "0.6" -windows = { version = "0.62", features = ["Win32_UI_Shell"] } - [target.'cfg(target_os = "windows")'.build-dependencies] winres = "0.1" diff --git a/desktop/platform/win/src/file_associations.rs b/desktop/platform/win/src/file_associations.rs deleted file mode 100644 index a7a63d1075..0000000000 --- a/desktop/platform/win/src/file_associations.rs +++ /dev/null @@ -1,103 +0,0 @@ -// This is a bit of a hack to get file associations working without an installer. -// TODO: Replace this with a proper installer that can set up file associations. - -use std::env; -use std::path::Path; - -use windows::Win32::UI::Shell::{SHCNE_ASSOCCHANGED, SHCNF_IDLIST, SHChangeNotify}; -use windows_registry::CURRENT_USER; - -const PROG_ID: &str = "Graphite.Document"; -const EXECUTABLE_NAME: &str = "Graphite.exe"; -const APP_FRIENDLY_NAME: &str = "Graphite"; -const DOCUMENT_FRIENDLY_NAME: &str = "Graphite Document"; -const MIME_TYPE: &str = "application/graphite+json"; -const FILE_EXTENSION: &str = ".graphite"; -const SUPPORTED_EXTENSIONS: &[&str] = &[FILE_EXTENSION, ".svg", ".png", ".jpg", ".jpeg"]; - -pub fn write() { - if let Err(e) = FileAssociationWriter::new(&env::current_exe().unwrap()) - .document_type(PROG_ID, DOCUMENT_FRIENDLY_NAME) - .application(EXECUTABLE_NAME, APP_FRIENDLY_NAME, SUPPORTED_EXTENSIONS) - .extension(FILE_EXTENSION, PROG_ID, MIME_TYPE) - .write() - { - eprintln!("Failed to register file associations: {e}"); - } -} - -struct FileAssociationWriter { - open_command: String, - icon_value: String, - entries: Vec, -} - -struct RegistryEntry { - path: String, - name: String, - value: String, -} - -impl FileAssociationWriter { - fn new(executable: &Path) -> Self { - let exe_string = executable.to_string_lossy(); - Self { - open_command: format!("\"{exe_string}\" \"%1\""), - icon_value: format!("{exe_string},0"), - entries: Vec::new(), - } - } - - fn document_type(mut self, prog_id: &str, friendly_name: &str) -> Self { - let base = format!("Software\\Classes\\{prog_id}"); - self.push(&base, "", friendly_name); - self.push(&format!("{base}\\DefaultIcon"), "", &self.icon_value.clone()); - self.push(&format!("{base}\\shell\\open\\command"), "", &self.open_command.clone()); - self - } - - fn application(mut self, executable_name: &str, friendly_name: &str, supported_extensions: &[&str]) -> Self { - let base = format!("Software\\Classes\\Applications\\{executable_name}"); - self.push(&base, "FriendlyAppName", friendly_name); - self.push(&format!("{base}\\shell\\open\\command"), "", &self.open_command.clone()); - - let supported_path = format!("{base}\\SupportedTypes"); - for extension in supported_extensions { - self.push(&supported_path, extension, ""); - } - self - } - - fn extension(mut self, extension: &str, prog_id: &str, mime_type: &str) -> Self { - let path = format!("Software\\Classes\\{extension}"); - self.push(&path, "", prog_id); - self.push(&path, "Content Type", mime_type); - self - } - - fn push(&mut self, path: &str, name: &str, value: &str) { - self.entries.push(RegistryEntry { - path: path.to_owned(), - name: name.to_owned(), - value: value.to_owned(), - }); - } - - fn write(self) -> windows_registry::Result<()> { - let mut changed = false; - - for entry in &self.entries { - let key = CURRENT_USER.create(&entry.path)?; - if key.get_string(&entry.name).ok().as_deref() == Some(entry.value.as_str()) { - continue; - } - key.set_string(&entry.name, &entry.value)?; - changed = true; - } - - if changed { - unsafe { SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, None, None) }; - } - Ok(()) - } -} diff --git a/desktop/platform/win/src/main.rs b/desktop/platform/win/src/main.rs index 646b77a349..f683b80b2f 100644 --- a/desktop/platform/win/src/main.rs +++ b/desktop/platform/win/src/main.rs @@ -1,11 +1,5 @@ #![windows_subsystem = "windows"] -#[cfg(target_os = "windows")] -mod file_associations; - fn main() { - #[cfg(target_os = "windows")] - file_associations::write(); - graphite_desktop::start(); }