Repository: denoland/deno
Pull request: feat: `deno compile --icon <ico>`

Add `--icon` flag to set deno compile'd executable icon on Windows.


```
deno compile --icon icon.ico game.tsx
```

Depends on https://github.com/denoland/sui/pull/24

<img width="447" alt="image" src="https://github.com/user-attachments/assets/7f6f3aa0-6872-4975-ae9d-06701b7684b8">

Closes https://github.com/denoland/deno/issues/8912

Changed files:
filename: Cargo.lock
patch: @@ -4169,9 +4169,9 @@ dependencies = [
 
 [[package]]
 name = "libsui"
-version = "0.1.0"
+version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c2fedcf6cb4dd935f94a90e1c4300c727fe7112b8455615e902828c7401f84d"
+checksum = "2d0f34a58599483cd44a31ff3622dbcca0c50679af60f98b705069dc729e70cf"
 dependencies = [
  "editpe",
  "libc",

filename: cli/Cargo.toml
patch: @@ -79,7 +79,7 @@ deno_semver = "=0.5.10"
 deno_task_shell = "=0.17.0"
 deno_terminal.workspace = true
 eszip = "=0.73.0"
-libsui = "0.1.0"
+libsui = "0.3.0"
 napi_sym.workspace = true
 node_resolver.workspace = true
 

filename: cli/args/flags.rs
patch: @@ -121,6 +121,7 @@ pub struct CompileFlags {
   pub args: Vec<String>,
   pub target: Option<String>,
   pub no_terminal: bool,
+  pub icon: Option<String>,
   pub include: Vec<String>,
 }
 
@@ -1769,6 +1770,12 @@ supported in canary.
             .help("Hide terminal on Windows")
             .action(ArgAction::SetTrue),
         )
+        .arg(
+          Arg::new("icon")
+            .long("icon")
+            .help("Set the icon of the executable on Windows (.ico)")
+            .value_parser(value_parser!(String))
+        )
         .arg(executable_ext_arg())
         .arg(env_file_arg())
         .arg(script_arg().required(true).trailing_var_arg(true))
@@ -3891,6 +3898,7 @@ fn compile_parse(flags: &mut Flags, matches: &mut ArgMatches) {
   let args = script.collect();
   let output = matches.remove_one::<String>("output");
   let target = matches.remove_one::<String>("target");
+  let icon = matches.remove_one::<String>("icon");
   let no_terminal = matches.get_flag("no-terminal");
   let include = match matches.remove_many::<String>("include") {
     Some(f) => f.collect(),
@@ -3904,6 +3912,7 @@ fn compile_parse(flags: &mut Flags, matches: &mut ArgMatches) {
     args,
     target,
     no_terminal,
+    icon,
     include,
   });
 }
@@ -9554,6 +9563,7 @@ mod tests {
           args: vec![],
           target: None,
           no_terminal: false,
+          icon: None,
           include: vec![]
         }),
         type_check_mode: TypeCheckMode::Local,
@@ -9565,7 +9575,7 @@ mod tests {
   #[test]
   fn compile_with_flags() {
     #[rustfmt::skip]
-    let r = flags_from_vec(svec!["deno", "compile", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--no-check", "--unsafely-ignore-certificate-errors", "--reload", "--lock", "lock.json", "--lock-write", "--cert", "example.crt", "--cached-only", "--location", "https:foo", "--allow-read", "--allow-net", "--v8-flags=--help", "--seed", "1", "--no-terminal", "--output", "colors", "--env=.example.env", "https://examples.deno.land/color-logging.ts", "foo", "bar", "-p", "8080"]);
+    let r = flags_from_vec(svec!["deno", "compile", "--import-map", "import_map.json", "--no-remote", "--config", "tsconfig.json", "--no-check", "--unsafely-ignore-certificate-errors", "--reload", "--lock", "lock.json", "--lock-write", "--cert", "example.crt", "--cached-only", "--location", "https:foo", "--allow-read", "--allow-net", "--v8-flags=--help", "--seed", "1", "--no-terminal", "--icon", "favicon.ico", "--output", "colors", "--env=.example.env", "https://examples.deno.land/color-logging.ts", "foo", "bar", "-p", "8080"]);
     assert_eq!(
       r.unwrap(),
       Flags {
@@ -9576,6 +9586,7 @@ mod tests {
           args: svec!["foo", "bar", "-p", "8080"],
           target: None,
           no_terminal: true,
+          icon: Some(String::from("favicon.ico")),
           include: vec![]
         }),
         import_map_path: Some("import_map.json".to_string()),

filename: cli/standalone/binary.rs
patch: @@ -187,10 +187,19 @@ fn write_binary_bytes(
 
   let target = compile_flags.resolve_target();
   if target.contains("linux") {
-    libsui::Elf::new(&original_bin).append(&writer, &mut file_writer)?;
+    libsui::Elf::new(&original_bin).append(
+      "d3n0l4nd",
+      &writer,
+      &mut file_writer,
+    )?;
   } else if target.contains("windows") {
-    libsui::PortableExecutable::from(&original_bin)?
-      .write_resource("d3n0l4nd", writer)?
+    let mut pe = libsui::PortableExecutable::from(&original_bin)?;
+    if let Some(icon) = compile_flags.icon.as_ref() {
+      let icon = std::fs::read(icon)?;
+      pe = pe.set_icon(&icon)?;
+    }
+
+    pe.write_resource("d3n0l4nd", writer)?
       .build(&mut file_writer)?;
   } else if target.contains("darwin") {
     libsui::Macho::from(original_bin)?
@@ -370,6 +379,15 @@ impl<'a> DenoCompileBinaryWriter<'a> {
       }
       set_windows_binary_to_gui(&mut original_binary)?;
     }
+    if compile_flags.icon.is_some() {
+      let target = compile_flags.resolve_target();
+      if !target.contains("windows") {
+        bail!(
+          "The `--icon` flag is only available when targeting Windows (current: {})",
+          target,
+        )
+      }
+    }
     self.write_standalone_binary(
       writer,
       original_binary,

filename: cli/tools/compile.rs
patch: @@ -361,6 +361,7 @@ mod test {
         args: Vec::new(),
         target: Some("x86_64-unknown-linux-gnu".to_string()),
         no_terminal: false,
+        icon: None,
         include: vec![],
       },
       &std::env::current_dir().unwrap(),
@@ -385,6 +386,7 @@ mod test {
         args: Vec::new(),
         target: Some("x86_64-pc-windows-msvc".to_string()),
         include: vec![],
+        icon: None,
         no_terminal: false,
       },
       &std::env::current_dir().unwrap(),