mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-10-02 15:58:11 +08:00
WIP
This commit is contained in:
Generated
+54
-14
@@ -411,6 +411,18 @@ dependencies = [
|
|||||||
"libbz2-rs-sys",
|
"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]]
|
[[package]]
|
||||||
name = "calloop"
|
name = "calloop"
|
||||||
version = "0.14.3"
|
version = "0.14.3"
|
||||||
@@ -593,6 +605,17 @@ version = "1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
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]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -2107,7 +2130,9 @@ dependencies = [
|
|||||||
name = "graphite-desktop-bundle"
|
name = "graphite-desktop-bundle"
|
||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"cab",
|
||||||
"cef-dll-sys",
|
"cef-dll-sys",
|
||||||
|
"msi",
|
||||||
"plist",
|
"plist",
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
@@ -2138,8 +2163,6 @@ name = "graphite-desktop-platform-win"
|
|||||||
version = "0.0.0"
|
version = "0.0.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"graphite-desktop",
|
"graphite-desktop",
|
||||||
"windows 0.62.2",
|
|
||||||
"windows-registry 0.6.1",
|
|
||||||
"winres",
|
"winres",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2441,7 +2464,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-registry 0.5.3",
|
"windows-registry",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3023,6 +3046,12 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lzxd"
|
||||||
|
version = "0.2.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mac"
|
name = "mac"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -3151,6 +3180,18 @@ dependencies = [
|
|||||||
"windows-sys 0.59.0",
|
"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]]
|
[[package]]
|
||||||
name = "muda"
|
name = "muda"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
@@ -6069,6 +6110,16 @@ version = "0.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
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]]
|
[[package]]
|
||||||
name = "valuable"
|
name = "valuable"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -6850,17 +6901,6 @@ dependencies = [
|
|||||||
"windows-strings 0.4.2",
|
"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]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|||||||
@@ -14,3 +14,7 @@ cef-dll-sys = { workspace = true }
|
|||||||
[target.'cfg(target_os = "macos")'.dependencies]
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
plist = { version = "*" }
|
plist = { version = "*" }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
msi = "0.10"
|
||||||
|
cab = "0.6"
|
||||||
|
|||||||
+528
-15
@@ -1,61 +1,574 @@
|
|||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
use std::io::{Cursor, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use cab::{CabinetBuilder, CompressionType};
|
||||||
|
use msi::{Column, Insert, Package, PackageType, Value};
|
||||||
|
|
||||||
use crate::common::*;
|
use crate::common::*;
|
||||||
|
|
||||||
const EXECUTABLE: &str = "Graphite.exe";
|
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>> {
|
pub fn main() -> Result<(), Box<dyn Error>> {
|
||||||
let app_bin = build_bin("graphite-desktop-platform-win", None)?;
|
let app_bin = build_bin("graphite-desktop-platform-win", None)?;
|
||||||
|
let profile_path = profile_path();
|
||||||
let executable = bundle(&profile_path(), &app_bin);
|
let bundle_dir = bundle(&profile_path, &app_bin);
|
||||||
|
let msi_path = create_msi(&bundle_dir, &profile_path)?;
|
||||||
|
|
||||||
// TODO: Consider adding more useful cli
|
// TODO: Consider adding more useful cli
|
||||||
let args: Vec<String> = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
if let Some(pos) = args.iter().position(|a| a == "open") {
|
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();
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bundle(out_dir: &Path, app_bin: &Path) -> PathBuf {
|
fn bundle(out_dir: &Path, app_bin: &Path) -> PathBuf {
|
||||||
let app_dir = out_dir.join(APP_NAME);
|
let app_dir = out_dir.join(APP_NAME);
|
||||||
|
|
||||||
clean_dir(&app_dir);
|
clean_dir(&app_dir);
|
||||||
|
|
||||||
copy_dir(&cef_path(), &app_dir);
|
copy_dir(&cef_path(), &app_dir);
|
||||||
|
|
||||||
if let Err(e) = remove_unnecessary_cef_files(&app_dir) {
|
if let Err(e) = remove_unnecessary_cef_files(&app_dir) {
|
||||||
eprintln!("Failed to remove unnecessary CEF files: {}", e);
|
eprintln!("Failed to remove unnecessary CEF files: {}", e);
|
||||||
}
|
}
|
||||||
|
fs::copy(app_bin, app_dir.join(EXECUTABLE)).unwrap();
|
||||||
let bin_path = app_dir.join(EXECUTABLE);
|
app_dir
|
||||||
fs::copy(app_bin, &bin_path).unwrap();
|
|
||||||
|
|
||||||
bin_path
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn remove_unnecessary_cef_files(app_dir: &Path) -> Result<(), Box<dyn Error>> {
|
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("cmake"))?;
|
||||||
fs::remove_dir_all(app_dir.join("include"))?;
|
fs::remove_dir_all(app_dir.join("include"))?;
|
||||||
fs::remove_dir_all(app_dir.join("libcef_dll"))?;
|
fs::remove_dir_all(app_dir.join("libcef_dll"))?;
|
||||||
|
|
||||||
for entry in fs::read_dir(app_dir.join("locales"))? {
|
for entry in fs::read_dir(app_dir.join("locales"))? {
|
||||||
let path = entry?.path();
|
let path = entry?.path();
|
||||||
if path.is_file() && path.file_name() != Some("en-US.pak".as_ref()) {
|
if path.is_file() && path.file_name() != Some("en-US.pak".as_ref()) {
|
||||||
fs::remove_file(path)?;
|
fs::remove_file(path)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fs::remove_file(app_dir.join("archive.json"))?;
|
fs::remove_file(app_dir.join("archive.json"))?;
|
||||||
fs::remove_file(app_dir.join("CMakeLists.txt"))?;
|
fs::remove_file(app_dir.join("CMakeLists.txt"))?;
|
||||||
fs::remove_file(app_dir.join("bootstrapc.exe"))?;
|
fs::remove_file(app_dir.join("bootstrapc.exe"))?;
|
||||||
fs::remove_file(app_dir.join("bootstrap.exe"))?;
|
fs::remove_file(app_dir.join("bootstrap.exe"))?;
|
||||||
fs::remove_file(app_dir.join("libcef.lib"))?;
|
fs::remove_file(app_dir.join("libcef.lib"))?;
|
||||||
fs::remove_file(app_dir.join("CREDITS.html"))?;
|
fs::remove_file(app_dir.join("CREDITS.html"))?;
|
||||||
|
|
||||||
Ok(())
|
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)]
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,9 +15,5 @@ path = "src/main.rs"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
graphite-desktop = { path = "../.." }
|
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]
|
[target.'cfg(target_os = "windows")'.build-dependencies]
|
||||||
winres = "0.1"
|
winres = "0.1"
|
||||||
|
|||||||
@@ -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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,5 @@
|
|||||||
#![windows_subsystem = "windows"]
|
#![windows_subsystem = "windows"]
|
||||||
|
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
mod file_associations;
|
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
file_associations::write();
|
|
||||||
|
|
||||||
graphite_desktop::start();
|
graphite_desktop::start();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user