use std::collections::VecDeque;
use std::{
env, fs,
path::{Path, PathBuf},
process::exit,
};
use uxn_tal::resolve_entry_from_url;
use uxn_tal::bkend_uxn::{ensure_docker_uxn_image, ensure_uxn_repo};
use uxn_tal::bkend_uxn38::{ensure_docker_uxn38_image, ensure_uxn38_repo};
use uxn_tal::bkend_buxn::{ensure_buxn_repo, ensure_docker_buxn_image};
use uxn_tal::chocolatal;
use uxn_tal::debug;
use uxn_tal::bkend_drif::ensure_drifblim_repo;
use uxn_tal::dis_uxndis::ensure_uxndis_repo;
use uxn_tal::{Assembler, AssemblerError};
use std::process::Command;
use std::io::Write;
fn main() {
if let Err(e) = real_main() {
eprintln!("error: {e}");
exit(1);
}
}
fn real_main() -> Result<(), AssemblerError> {
let mut args: Vec<String> = env::args().skip(1).collect();
let root_dir = &std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
println!("root dir: {:?}", root_dir);
let mut pre = false;
let mut preprocess_only = false;
let mut want_version = false;
let mut want_verbose = false;
let mut want_cmp = false;
let mut want_cmp_pp = false;
let mut want_stdin = false;
let mut drif_mode = false;
let mut rust_iface: Option<String> = None; let mut use_root: Option<PathBuf> = None; let mut run_after_assembly: Option<String> = None; let mut run_after_cwd: Option<PathBuf> = None;
let mut positional: Vec<String> = Vec::new();
if args.len() > 0 {
let raw_url = &args[0];
if raw_url == "uxntal:"
|| raw_url == "uxntal:/"
|| raw_url == "uxntal://"
|| raw_url == "uxntal:///"
{
std::process::exit(0);
}
if raw_url.starts_with("uxntal://") {
let (entry_local, rom_dir) = match resolve_entry_from_url(raw_url) {
Ok(v) => v,
Err(e) => { eprintln!("Failed to resolve uxntal URL: {}", e); std::process::exit(1); }
};
println!("Resolved entry: {}", entry_local.display());
run_after_assembly = Some("cardinal-gui".to_owned());
run_after_cwd = Some(rom_dir.clone());
args[0] = entry_local
.strip_prefix(r"\\?\")
.unwrap_or(&entry_local)
.display()
.to_string();
}
}
println!("args: {:?}", args);
if args.len() > 0 && args[0] == "--register" {
register_protocol_per_user()?;
println!("You need to `cargo install e_window cardinal-gui`. Ctrl+c to exit, or press return to run the install.");
print!("Press Enter to continue...");
std::io::stdout().flush().ok();
let _ = std::io::stdin().read_line(&mut String::new());
let status = Command::new("cargo")
.args(["install", "e_window", "cardinal-gui"])
.status();
match status {
Ok(s) if s.success() => {
println!("Successfully ran: cargo install e_window cardinal-gui");
}
Ok(s) => {
eprintln!("cargo install exited with status: {}", s);
}
Err(e) => {
eprintln!("Failed to run cargo install: {}", e);
}
}
return Ok(());
}
for a in args.drain(..) {
if a == "--version" || a == "-V" {
want_version = true;
} else if a == "--verbose" || a == "-v" {
want_verbose = true;
} else if a == "--cmp" {
want_cmp = true;
} else if a == "--cmp-pp" {
want_cmp_pp = true;
} else if a == "--stdin" {
want_stdin = true;
} else if a.starts_with("--rust-interface") {
if let Some(eq) = a.find('=') {
let name = a[eq + 1..].trim();
if !name.is_empty() {
rust_iface = Some(name.to_string());
} else {
rust_iface = Some("symbols".to_string());
}
} else {
rust_iface = Some("symbols".to_string());
}
} else if a.starts_with("--r") {
if let Some(eq) = a.find('=') {
let name = a[eq + 1..].trim();
if !name.is_empty() {
use_root = Some(PathBuf::from(name));
} else {
use_root = Some(root_dir.clone());
}
} else {
use_root = Some(root_dir.clone());
}
} else if a == "--pre" {
pre = true;
} else if a == "--preprocess" {
preprocess_only = true;
} else if a == "--drif" || a == "--drifblim" {
drif_mode = true;
} else if a.starts_with('-') {
eprintln!("unknown flag: {a}");
print_usage();
exit(2);
} else {
positional.push(a);
}
}
if want_cmp_pp {
if positional.is_empty() {
eprintln!("missing input file for --cmp-pp");
print_usage();
exit(2);
}
let raw_input = &positional[0];
let input_path = match resolve_input_path(raw_input) {
Some(p) => p,
None => {
return Err(simple_err(
std::path::Path::new(raw_input),
"input file not found (tried direct, +.tal, multi-root recursive scan)",
));
}
};
if let Err(e) = debug::compare_preprocessors(&input_path.display().to_string(), &root_dir) {
eprintln!("compare_preprocessors error: {e}");
exit(1);
}
exit(0);
}
if want_version {
println!("uxntal {} (library)", env!("CARGO_PKG_VERSION"));
return Ok(());
}
let mut source = String::new();
let canon_input_p;
let mut input_from_stdin = false;
let mut input_is_rom = false;
if want_stdin || (!positional.is_empty() && positional[0] == "/dev/stdin") {
use std::io::{self, Read};
io::stdin().read_to_string(&mut source).map_err(|e| {
simple_err(
Path::new("/dev/stdin"),
&format!("failed to read from stdin: {e}"),
)
})?;
canon_input_p = PathBuf::from("/dev/stdin");
input_from_stdin = true;
} else {
if positional.is_empty() {
eprintln!("missing input file");
print_usage();
exit(2);
}
let raw_input = &positional[0];
let input_path = match resolve_input_path(raw_input) {
Some(p) => p,
None => {
return Err(simple_err(
std::path::Path::new(raw_input),
"input file not found (tried direct, +.tal, +.rom, multi-root recursive scan)",
));
}
};
canon_input_p = input_path
.canonicalize()
.unwrap_or_else(|_| input_path.clone());
if let Some(ext) = canon_input_p.extension() {
if ext == "rom" {
input_is_rom = true;
}
}
if use_root.is_some() {
if let Some(root) = &use_root {
if let Err(e) = std::env::set_current_dir(root) {
if want_verbose {
eprintln!("warning: failed to chdir to {}: {e}", root.display());
}
} else if want_verbose {
eprintln!("Changed working directory to {}", root.display());
}
}
} else if let Some(parent) = canon_input_p.parent() {
if let Err(e) = std::env::set_current_dir(parent) {
if want_verbose {
eprintln!("warning: failed to chdir to {}: {e}", parent.display());
}
} else if want_verbose {
eprintln!("Changed working directory to {}", parent.display());
}
}
if !input_is_rom {
source = fs::read_to_string(&canon_input_p)
.map_err(|e| simple_err(&canon_input_p, &format!("failed to read: {e}")))?;
}
}
let rom_path_p = if positional.len() > 1 {
let supplied = PathBuf::from(&positional[1]);
if supplied.is_absolute() {
supplied
} else {
std::env::current_dir()
.unwrap_or_else(|_| PathBuf::from("."))
.join(supplied)
}
} else if input_from_stdin {
PathBuf::from("out.rom")
} else {
canon_input_p.with_extension("rom")
};
let rom_path_str = rom_path_p.display().to_string();
let rom_path = rom_path_str.strip_prefix(r"\\?\").unwrap_or(&rom_path_str);
let canon_input_str = canon_input_p.display().to_string();
let canon_input = canon_input_str.strip_prefix(r"\\?\").unwrap_or(&canon_input_str);
if want_verbose {
eprintln!("Resolved input : {}", canon_input);
eprintln!("Output ROM : {}", rom_path);
if rust_iface.is_some() {
eprintln!("Rust interface : enabled");
}
}
let processed_src = if !input_is_rom {
if pre {
match chocolatal::preprocess(&source, &canon_input, &root_dir) {
Ok(s) => s,
Err(e) => {
eprintln!("Preprocessor error: {:?}", e);
std::process::exit(1);
}
}
} else {
source.clone()
}
} else {
String::new()
};
if preprocess_only && !input_is_rom {
print!("{}", processed_src);
std::process::exit(0);
} else if preprocess_only && input_is_rom {
eprintln!("Cannot preprocess a .rom file");
std::process::exit(1);
}
let mut asm = if drif_mode {
Assembler::with_drif_mode(true)
} else {
Assembler::new()
};
if want_cmp {
println!("{:?}", ensure_drifblim_repo());
let _ = ensure_uxndis_repo();
let _ = ensure_buxn_repo();
let _ = ensure_docker_buxn_image();
let _ = ensure_uxn38_repo();
let _ = ensure_docker_uxn38_image();
let _ = ensure_uxn_repo();
let _ = ensure_docker_uxn_image();
let dbg = if drif_mode {
debug::DebugAssembler::with_drif_mode(true)
} else {
debug::DebugAssembler::default()
};
let rel_path = match canon_input_p.strip_prefix(std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))) {
Ok(p) => p.display().to_string(),
Err(_) => canon_input_p.display().to_string(),
};
eprintln!("Relative path to input: {}", rel_path);
let res = dbg.assemble_and_compare( &rel_path,&processed_src, true);
return res.map(|_| ());
}
if input_is_rom {
if rom_path_p.exists() {
println!("ROM already exists at {}", rom_path);
} else {
fs::copy(&canon_input_p, &rom_path_p)
.map_err(|e| simple_err(Path::new(rom_path), &format!("failed to copy rom: {e}")))?;
if want_verbose {
eprintln!("Copied ROM ({} bytes)", fs::metadata(&rom_path_p).map(|m| m.len()).unwrap_or(0));
} else {
println!("{} ({} bytes)", rom_path, fs::metadata(&rom_path_p).map(|m| m.len()).unwrap_or(0));
}
}
} else if run_after_assembly.is_some() {
if rom_path_p.exists() {
println!("ROM already exists at {}", rom_path);
} else {
let rom = asm.assemble(&processed_src, Some(canon_input.to_owned()))?;
fs::write(&rom_path, &rom)
.map_err(|e| simple_err(Path::new(rom_path), &format!("failed to write rom: {e}")))?;
if want_verbose {
eprintln!("Wrote ROM ({} bytes)", rom.len());
} else {
println!("{} ({} bytes)", rom_path, rom.len());
}
}
} else {
let rom = asm.assemble(&processed_src, Some(canon_input.to_owned()))?;
fs::write(&rom_path, &rom)
.map_err(|e| simple_err(Path::new(rom_path), &format!("failed to write rom: {e}")))?;
if want_verbose {
eprintln!("Wrote ROM ({} bytes)", rom.len());
} else {
println!("{} ({} bytes)", rom_path, rom.len());
}
}
if let Some(module_name) = rust_iface {
let mod_src = uxn_tal::generate_rust_interface_module(&asm, &module_name);
let iface_path = rom_path_p.with_extension("rom.symbols.rs");
fs::write(&iface_path, mod_src).map_err(|e| {
simple_err(&iface_path, &format!("failed to write rust interface: {e}"))
})?;
if want_verbose {
eprintln!("Wrote Rust interface module: {}", iface_path.display());
} else {
println!("{}", iface_path.display());
}
}
if let Some(cmd) = run_after_assembly {
let cmd_name = cmd.clone();
let path_to_emu = which::which(&cmd_name)
.or_else(|_| {
if let Ok(home) = std::env::var("HOME") {
let p = PathBuf::from(format!("{}/.cargo/bin/{}", home, &cmd_name));
if p.exists() {
Ok(p)
} else {
Err(())
}
} else {
Err(())
}
})
.map_err(|_| simple_err(Path::new("."), &format!("{cmd_name} not found in PATH or ~/.cargo/bin")))?;
println!("Running post-assembly command: {}", cmd);
let dir_str = run_after_cwd
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| std::env::current_dir().map(|p| p.display().to_string()).unwrap_or_else(|_| ".".to_string()));
println!("In directory: {}", dir_str);
let status = Command::new(path_to_emu)
.arg(&rom_path)
.current_dir(run_after_cwd.unwrap_or_else(|| PathBuf::from(".")))
.status();
match status {
Ok(s) if s.success() => {
println!("Ran post-assembly command: {}", cmd);
}
Ok(s) => {
eprintln!("Post-assembly command exited with status: {}", s);
}
Err(e) => {
eprintln!("Failed to run post-assembly command: {}", e);
}
}
}
Ok(())
}
fn print_usage() {
eprintln!(
"Usage:
uxntal [flags] <input.tal|/dev/stdin> [output.rom]
Flags:
--version, -V Show version and exit
--verbose, -v Verbose output
--rust-interface[=M] Emit Rust symbols module (default module name: symbols)
--cmp Compare disassembly for all backends
--stdin Read input.tal from stdin
--cmp-pp Compare preprocessor output (Rust vs deluge)
--pre Enable preprocessing
--preprocess Print preprocessed output and exit
--drif, --drifblim Enable drifblim-compatible mode (optimizations, reference resolution)
--r, --root[=DIR] Set root directory for includes (default: current dir)
--register Register uxntal as a file handler (Windows only)
--r, --root[=DIR] Set root directory for includes (default: current dir)
--register Register uxntal as a file handler (Windows only)
--help, -h Show this help
Behavior:
If output.rom omitted, use input path with .rom extension, or 'out.rom' if reading from stdin.
You can also pass /dev/stdin as the input filename to read from stdin.
Rust interface file path: <output>.rom.symbols.rs"
);
}
fn simple_err(path: &std::path::Path, msg: &str) -> AssemblerError {
AssemblerError::SyntaxError {
path: path.display().to_string(),
line: 0,
position: 0,
message: msg.to_string(),
source_line: String::new(),
}
}
fn resolve_input_path(arg: &str) -> Option<PathBuf> {
let direct = PathBuf::from(arg);
if direct.exists() {
return Some(direct);
}
if direct.extension().is_none() {
let with_tal = direct.with_extension("tal");
if with_tal.exists() {
return Some(with_tal);
}
let with_rom = direct.with_extension("rom");
if with_rom.exists() {
return Some(with_rom);
}
}
let mut roots: Vec<PathBuf> = Vec::new();
if let Ok(cwd) = std::env::current_dir() {
roots.push(cwd);
}
roots.push(PathBuf::from(env!("CARGO_MANIFEST_DIR"))); if let Some(parent) = Path::new(env!("CARGO_MANIFEST_DIR")).parent() {
roots.push(parent.to_path_buf());
if let Some(grand) = parent.parent() {
roots.push(grand.to_path_buf());
}
}
roots.sort();
roots.dedup();
if arg.contains('/') || arg.contains('\\') {
return None;
}
for root in roots {
if !root.is_dir() {
continue;
}
if let Some(found) = recursive_find(&root, arg, 12_000) {
return Some(found);
}
let alt_tal = format!("{arg}.tal");
if let Some(found) = recursive_find(&root, &alt_tal, 12_000) {
return Some(found);
}
let alt_rom = format!("{arg}.rom");
if let Some(found) = recursive_find(&root, &alt_rom, 12_000) {
return Some(found);
}
}
None
}
fn recursive_find(root: &Path, needle: &str, cap: usize) -> Option<PathBuf> {
let mut q = VecDeque::new();
q.push_back(root.to_path_buf());
let mut visited = 0usize;
while let Some(dir) = q.pop_front() {
if visited >= cap {
break;
}
visited += 1;
let rd = fs::read_dir(&dir).ok()?;
for entry in rd.flatten() {
let p = entry.path();
if p.is_dir() {
if q.len() < 4096 {
q.push_back(p);
}
continue;
}
if let Some(name) = p.file_name().and_then(|s| s.to_str()) {
if name == needle {
return Some(p);
}
}
}
}
None
}
#[cfg(target_os = "macos")]
fn register_protocol_per_user() -> std::io::Result<()> {
use std::io::{Write, stdin, stdout};
use std::path::PathBuf;
use std::process::Command;
if which::which("xcrun").is_err() || which::which("swiftc").is_err() {
eprintln!("Error: Xcode command line tools are required to register the protocol handler.");
eprintln!("Please install them with: xcode-select --install");
return Ok(());
}
let uxntal_path = which::which("uxntal").expect("Could not find uxntal in PATH");
let version = env!("CARGO_PKG_VERSION");
let home = std::env::var("HOME").unwrap();
let temp_dir = PathBuf::from(format!("{}/.uxntal_swift_launcher", home));
let app_delegate_file = temp_dir.join("AppDelegate.swift");
let main_file = temp_dir.join("main.swift");
let plist_file = temp_dir.join("Info.plist");
let app_name = "uxntal-launcher";
let app_bundle = temp_dir.join(format!("{app_name}.app"));
let _ = fs::remove_dir_all(&temp_dir);
fs::create_dir_all(&temp_dir)?;
let swift_app_delegate = format!(r#"
import Cocoa
class AppDelegate: NSObject, NSApplicationDelegate {{
func application(_ application: NSApplication, open urls: [URL]) {{
for url in urls {{
let task = Process()
task.launchPath = "{bin_path}"
task.arguments = [url.absoluteString]
task.launch()
}}
NSApp.terminate(nil)
}}
func applicationDidFinishLaunching(_ notification: Notification) {{
NSApp.terminate(nil)
}}
}}
"#, bin_path = uxntal_path.display());
let mut f = std::fs::File::create(&app_delegate_file)?;
f.write_all(swift_app_delegate.as_bytes())?;
let swift_main = r#"
import Cocoa
let delegate = AppDelegate()
NSApplication.shared.delegate = delegate
_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)
"#;
let mut f = std::fs::File::create(&main_file)?;
f.write_all(swift_main.as_bytes())?;
let plist = format!(r#"<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleName</key>
<string>uxntal-launcher</string>
<key>CFBundleIdentifier</key>
<string>uxntal.uxn-tal.launcher</string>
<key>CFBundleVersion</key>
<string>{version}</string>
<key>CFBundleShortVersionString</key>
<string>{version}</string>
<key>CFBundleExecutable</key>
<string>{app_name}</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>UXNTAL Protocol</string>
<key>CFBundleURLSchemes</key>
<array>
<string>uxntal</string>
</array>
</dict>
</array>
</dict>
</plist>
"#, version=version, app_name=app_name);
let mut f = std::fs::File::create(&plist_file)?;
f.write_all(plist.as_bytes())?;
let status = Command::new("xcrun")
.args([
"swiftc",
"-o", &format!("{}/{}", temp_dir.display(), app_name),
main_file.to_str().unwrap(),
app_delegate_file.to_str().unwrap(),
])
.status()?;
if !status.success() {
eprintln!("Failed to compile Swift launcher. Is Xcode command line tools installed?");
return Ok(());
}
let app_contents = app_bundle.join("Contents");
let macos_dir = app_contents.join("MacOS");
fs::create_dir_all(&macos_dir)?;
fs::copy(
temp_dir.join(app_name),
macos_dir.join(app_name),
)?;
fs::copy(&plist_file, app_contents.join("Info.plist"))?;
let user_app = PathBuf::from(format!("{}/Applications/uxntal.app", home));
if user_app.exists() {
println!("An existing uxntal.app was found at {}.", user_app.display());
print!("Do you want to remove it and create a new one? [y/N]: ");
stdout().flush().ok();
let mut answer = String::new();
stdin().read_line(&mut answer).ok();
if answer.trim().eq_ignore_ascii_case("y") {
fs::remove_dir_all(&user_app)?;
println!("Removed old uxntal.app.");
} else {
println!("Aborted by user.");
return Ok(());
}
}
fs::rename(&app_bundle, &user_app)?;
println!("Created uxntal.app at {}", user_app.display());
println!("Double click uxntal.app in ~/Applications to register the uxntal:// protocol.");
let _ = Command::new("open")
.arg(format!("{}/Applications", home))
.status();
Ok(())
}
#[cfg(not(target_os = "macos"))]
fn register_protocol_per_user() -> std::io::Result<()> {
#[cfg(windows)]
{
let exe = std::env::current_exe()?.display().to_string();
let cmd = format!(r#""{}" "%1""#, exe);
let status1 = Command::new("reg")
.args([
"add",
r"HKCU\Software\Classes\uxntal",
"/ve",
"/t",
"REG_SZ",
"/d",
"URL:UXNTAL Protocol",
"/f",
])
.status()?;
let status2 = Command::new("reg")
.args([
"add",
r"HKCU\Software\Classes\uxntal",
"/v",
"URL Protocol",
"/t",
"REG_SZ",
"/d",
"",
"/f",
])
.status()?;
let status3 = Command::new("reg")
.args([
"add",
r"HKCU\Software\Classes\uxntal\shell\open\command",
"/ve",
"/t",
"REG_SZ",
"/d",
&cmd,
"/f",
])
.status()?;
if status1.success() && status2.success() && status3.success() {
println!("Registered uxntal:// protocol for current user on Windows.");
} else {
eprintln!(
"Failed: {:?} {:?} {:?}",
status1.code(),
status2.code(),
status3.code()
);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to register protocol on Windows",
));
}
Ok(())
}
#[cfg(unix)]
{
let exe = std::env::current_exe()?.display().to_string();
let home_dir = std::env::var("HOME").map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("HOME environment variable not found: {}", e),
)
})?;
let desktop_file_path = format!("{}/.local/share/applications/uxntal.desktop", home_dir);
let mime_file_path = format!(
"{}/.local/share/mime/packages/x-scheme-handler-uxntal.xml",
home_dir
);
if let Some(parent) = std::path::Path::new(&mime_file_path).parent() {
std::fs::create_dir_all(parent)?;
}
let mime_content = r#"<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="x-scheme-handler/uxntal">
<comment>UXNTAL Protocol</comment>
<glob pattern="uxntal://*"/>
</mime-type>
</mime-info>
"#;
let mut mime_file = std::fs::File::create(&mime_file_path)?;
mime_file.write_all(mime_content.as_bytes())?;
let desktop_content = format!(
r#"[Desktop Entry]
Name=UXNTAL Handler
Exec={} %u
Type=Application
Terminal=false
MimeType=x-scheme-handler/uxntal;
NoDisplay=true
"#,
exe
);
if let Some(parent) = std::path::Path::new(&desktop_file_path).parent() {
std::fs::create_dir_all(parent)?;
}
let mut desktop_file = std::fs::File::create(&desktop_file_path)?;
desktop_file.write_all(desktop_content.as_bytes())?;
let status1 = Command::new("xdg-mime")
.args(["install", "--mode", "user", &mime_file_path])
.status()?;
let status2 = Command::new("xdg-mime")
.args(["default", "uxntal.desktop", "x-scheme-handler/uxntal"])
.status()?;
let status3 = Command::new("update-desktop-database")
.arg(format!("{}/.local/share/applications", home_dir))
.status()?;
if status1.success() && status2.success() && status3.success() {
println!("Registered uxntal:// protocol for current user on Ubuntu.");
} else {
eprintln!(
"Failed: xdg-mime install status: {:?}, xdg-mime default status: {:?}, update-desktop-database status: {:?}",
status1.code(),
status2.code(),
status3.code()
);
let _ = std::fs::remove_file(&desktop_file_path);
let _ = std::fs::remove_file(&mime_file_path);
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to register protocol on Ubuntu",
));
}
println!("You need to `cargo install e_window cardinal-gui`. Ctrl+C to exit, or press Enter to run the install.");
print!("Press Enter to continue...");
std::io::stdout().flush()?;
let _ = std::io::stdin().read_line(&mut String::new())?;
let status = Command::new("cargo")
.args(["install", "e_window", "cardinal-gui"])
.status()?;
if status.success() {
println!("Successfully ran: cargo install e_window cardinal-gui");
} else {
eprintln!("cargo install exited with status: {:?}", status.code());
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to run cargo install",
));
}
Ok(())
}
#[cfg(not(any(windows, unix)))]
{
Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Protocol registration is only supported on Windows and Unix-like systems (e.g., Ubuntu)",
))
}
}