This commit is contained in:
Timon
2026-05-10 14:35:14 +00:00
parent b116b9f3fd
commit e968abf0a9
6 changed files with 586 additions and 142 deletions

68
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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<dyn Error>> {
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<String> = 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<dyn Error>> {
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<String>,
default_dir: String,
}
struct AppComponent {
component_id: String,
component_guid: String,
dir_id: String,
key_file_id: String,
}
struct Entries {
files: Vec<AppFile>,
directories: Vec<AppDirectory>,
components: Vec<AppComponent>,
}
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<Entries, Box<dyn Error>> {
let mut raw_files: Vec<(PathBuf, PathBuf)> = Vec::new();
gather_files(bundle_dir, bundle_dir, &mut raw_files)?;
let mut all_dir_strs: BTreeSet<String> = 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<String, (String, String)> = 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<String, String> = BTreeMap::new();
let mut file_dir_strs: BTreeSet<String> = 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<AppComponent> = 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::<Result<_, _>>()?;
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<Vec<u8>, Box<dyn Error>> {
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<PathBuf, Box<dyn Error>> {
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<fs::File>) -> Result<(), Box<dyn Error>> {
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<fs::File>) -> Result<(), Box<dyn Error>> {
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<fs::File>, dirs: &[AppDirectory]) -> Result<(), Box<dyn Error>> {
let rows: Vec<Vec<Value>> = 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<fs::File>, components: &[AppComponent]) -> Result<(), Box<dyn Error>> {
let rows: Vec<Vec<Value>> = 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<fs::File>) -> Result<(), Box<dyn Error>> {
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<fs::File>, components: &[AppComponent]) -> Result<(), Box<dyn Error>> {
let rows: Vec<Vec<Value>> = 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<fs::File>, files: &[AppFile]) -> Result<(), Box<dyn Error>> {
let rows: Vec<Vec<Value>> = 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<fs::File>, last_sequence: u32) -> Result<(), Box<dyn Error>> {
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<fs::File>, components: &[AppComponent]) -> Result<(), Box<dyn Error>> {
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<Value>> = 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<Value> {
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<fs::File>) -> Result<(), Box<dyn Error>> {
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<Value> {
vec![Value::from(action), Value::Null, Value::from(sequence)]
}

View File

@@ -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"

View File

@@ -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<RegistryEntry>,
}
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(())
}
}

View File

@@ -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();
}