| 1 | module builder |
| 2 | |
| 3 | import os |
| 4 | |
| 5 | const windows_icon_group_resource_id = 1 |
| 6 | const max_windows_icon_dimension = 256 |
| 7 | |
| 8 | struct WindowsIconImage { |
| 9 | width u8 |
| 10 | height u8 |
| 11 | color_count u8 |
| 12 | planes u16 |
| 13 | bit_count u16 |
| 14 | bytes_in_res u32 |
| 15 | image_data []u8 |
| 16 | } |
| 17 | |
| 18 | struct WindowsIconSize { |
| 19 | width int |
| 20 | height int |
| 21 | } |
| 22 | |
| 23 | fn (b &Builder) ensure_windows_icon_flag_is_valid() { |
| 24 | if b.pref.icon_path == '' { |
| 25 | return |
| 26 | } |
| 27 | if b.pref.os != .windows || b.pref.build_mode == .build_module || b.pref.is_o |
| 28 | || b.pref.is_shared { |
| 29 | verror('`-icon` is supported only when building Windows executables') |
| 30 | } |
| 31 | if b.pref.generate_c_project != '' || b.pref.out_name.ends_with('.c') |
| 32 | || b.pref.out_name.ends_with('.js') || b.pref.should_output_to_stdout() { |
| 33 | verror('`-icon` cannot be used when emitting generated C/JS output instead of a Windows executable') |
| 34 | } |
| 35 | } |
| 36 | |
| 37 | fn (mut b Builder) prepare_cross_windows_icon_resource() !string { |
| 38 | if b.pref.icon_path == '' { |
| 39 | return '' |
| 40 | } |
| 41 | ico_path := b.prepare_windows_icon_ico_path()! |
| 42 | rc_path := b.get_vtmp_filename(b.pref.out_name, '.icon.rc') |
| 43 | obj_path := b.get_vtmp_filename(b.pref.out_name, '.icon.o') |
| 44 | os.write_file(rc_path, '1 ICON ${rc_quoted_string(ico_path)}\n')! |
| 45 | b.pref.cleanup_files << rc_path |
| 46 | b.pref.cleanup_files << obj_path |
| 47 | windres := b.find_windres()! |
| 48 | cmd := '${os.quoted_path(windres)} -i ${os.quoted_path(rc_path)} -o ${os.quoted_path(obj_path)} -O coff' |
| 49 | res := os.execute(cmd) |
| 50 | if res.exit_code != 0 { |
| 51 | return error('failed to compile Windows icon resource with `${windres}`: ${res.output.trim_space()}') |
| 52 | } |
| 53 | return obj_path |
| 54 | } |
| 55 | |
| 56 | fn (mut b Builder) prepare_windows_icon_ico_path() !string { |
| 57 | if b.pref.icon_path == '' { |
| 58 | return '' |
| 59 | } |
| 60 | icon_path := os.real_path(b.pref.icon_path) |
| 61 | if !os.is_file(icon_path) { |
| 62 | return error('icon file `${icon_path}` does not exist') |
| 63 | } |
| 64 | match os.file_ext(icon_path).to_lower_ascii() { |
| 65 | '.ico' { |
| 66 | return icon_path |
| 67 | } |
| 68 | '.png' { |
| 69 | png_bytes := os.read_bytes(icon_path)! |
| 70 | ico_bytes := png_to_ico_bytes(png_bytes)! |
| 71 | ico_path := b.get_vtmp_filename(b.pref.out_name, '.icon.ico') |
| 72 | os.write_file_array(ico_path, ico_bytes)! |
| 73 | b.pref.cleanup_files << ico_path |
| 74 | return ico_path |
| 75 | } |
| 76 | else { |
| 77 | return error('`-icon` accepts only `.ico` or `.png` files') |
| 78 | } |
| 79 | } |
| 80 | } |
| 81 | |
| 82 | fn (b &Builder) find_windres() !string { |
| 83 | compiler_dir := if b.pref.ccompiler.contains('/') || b.pref.ccompiler.contains('\\') { |
| 84 | os.dir(b.pref.ccompiler) |
| 85 | } else { |
| 86 | '' |
| 87 | } |
| 88 | compiler_name := executable_stem(os.file_name(b.pref.ccompiler)) |
| 89 | mut candidates := []string{} |
| 90 | for suffix in ['-gcc', '-clang', '-cc', '-g++', '-clang++'] { |
| 91 | if compiler_name.ends_with(suffix) { |
| 92 | candidates << '${compiler_name[..compiler_name.len - suffix.len]}-windres' |
| 93 | } |
| 94 | } |
| 95 | candidates << 'windres' |
| 96 | candidates << 'llvm-windres' |
| 97 | for candidate in candidates { |
| 98 | for name in [candidate, '${candidate}.exe'] { |
| 99 | if compiler_dir != '' { |
| 100 | full_path := os.join_path(compiler_dir, name) |
| 101 | if os.is_file(full_path) { |
| 102 | return full_path |
| 103 | } |
| 104 | } |
| 105 | if resolved := os.find_abs_path_of_executable(name) { |
| 106 | return resolved |
| 107 | } |
| 108 | } |
| 109 | } |
| 110 | return error('could not find `windres`, which is needed for `-icon` while cross-compiling to Windows') |
| 111 | } |
| 112 | |
| 113 | fn executable_stem(name string) string { |
| 114 | lower_name := name.to_lower_ascii() |
| 115 | if lower_name.ends_with('.exe') { |
| 116 | return name[..name.len - 4] |
| 117 | } |
| 118 | return name |
| 119 | } |
| 120 | |
| 121 | fn rc_quoted_string(path string) string { |
| 122 | return '"' + path.replace('\\', '\\\\').replace('"', '\\"') + '"' |
| 123 | } |
| 124 | |
| 125 | fn parse_ico_file(path string) ![]WindowsIconImage { |
| 126 | return parse_ico_bytes(os.read_bytes(path)!) |
| 127 | } |
| 128 | |
| 129 | fn parse_ico_bytes(data []u8) ![]WindowsIconImage { |
| 130 | if data.len < 6 { |
| 131 | return error('invalid icon file: missing ICO header') |
| 132 | } |
| 133 | if read_le_u16(data, 0) != 0 || read_le_u16(data, 2) != 1 { |
| 134 | return error('invalid icon file: expected an ICO header') |
| 135 | } |
| 136 | image_count := int(read_le_u16(data, 4)) |
| 137 | if image_count <= 0 { |
| 138 | return error('invalid icon file: no icon images were found') |
| 139 | } |
| 140 | if data.len < 6 + (image_count * 16) { |
| 141 | return error('invalid icon file: truncated icon directory') |
| 142 | } |
| 143 | mut images := []WindowsIconImage{cap: image_count} |
| 144 | for i := 0; i < image_count; i++ { |
| 145 | entry_offset := 6 + (i * 16) |
| 146 | image_size := int(read_le_u32(data, entry_offset + 8)) |
| 147 | image_offset := int(read_le_u32(data, entry_offset + 12)) |
| 148 | image_end := image_offset + image_size |
| 149 | if image_offset < 0 || image_size <= 0 || image_offset > data.len || image_end > data.len { |
| 150 | return error('invalid icon file: icon image ${i + 1} points outside the file') |
| 151 | } |
| 152 | images << WindowsIconImage{ |
| 153 | width: data[entry_offset] |
| 154 | height: data[entry_offset + 1] |
| 155 | color_count: data[entry_offset + 2] |
| 156 | planes: read_le_u16(data, entry_offset + 4) |
| 157 | bit_count: read_le_u16(data, entry_offset + 6) |
| 158 | bytes_in_res: u32(image_size) |
| 159 | image_data: data[image_offset..image_end].clone() |
| 160 | } |
| 161 | } |
| 162 | return images |
| 163 | } |
| 164 | |
| 165 | fn png_to_ico_bytes(png []u8) ![]u8 { |
| 166 | size := png_dimensions(png)! |
| 167 | mut ico := []u8{cap: 22 + png.len} |
| 168 | append_le_u16(mut ico, 0) |
| 169 | append_le_u16(mut ico, 1) |
| 170 | append_le_u16(mut ico, 1) |
| 171 | ico << u8(if size.width == max_windows_icon_dimension { 0 } else { size.width }) |
| 172 | ico << u8(if size.height == max_windows_icon_dimension { 0 } else { size.height }) |
| 173 | ico << u8(0) |
| 174 | ico << u8(0) |
| 175 | append_le_u16(mut ico, 1) |
| 176 | append_le_u16(mut ico, 32) |
| 177 | append_le_u32(mut ico, u32(png.len)) |
| 178 | append_le_u32(mut ico, u32(22)) |
| 179 | ico << png |
| 180 | return ico |
| 181 | } |
| 182 | |
| 183 | fn png_dimensions(png []u8) !WindowsIconSize { |
| 184 | if png.len < 24 { |
| 185 | return error('invalid PNG icon file: missing PNG header') |
| 186 | } |
| 187 | if png[0] != 0x89 || png[1] != `P` || png[2] != `N` || png[3] != `G` || png[4] != 0x0d |
| 188 | || png[5] != 0x0a || png[6] != 0x1a || png[7] != 0x0a { |
| 189 | return error('invalid PNG icon file: bad PNG signature') |
| 190 | } |
| 191 | if png[12] != `I` || png[13] != `H` || png[14] != `D` || png[15] != `R` { |
| 192 | return error('invalid PNG icon file: missing IHDR chunk') |
| 193 | } |
| 194 | width := int(read_be_u32(png, 16)) |
| 195 | height := int(read_be_u32(png, 20)) |
| 196 | if width <= 0 || height <= 0 || width > max_windows_icon_dimension |
| 197 | || height > max_windows_icon_dimension { |
| 198 | return error('PNG icons must be between 1x1 and 256x256 pixels') |
| 199 | } |
| 200 | return WindowsIconSize{ |
| 201 | width: width |
| 202 | height: height |
| 203 | } |
| 204 | } |
| 205 | |
| 206 | fn build_group_icon_resource(images []WindowsIconImage) []u8 { |
| 207 | mut data := []u8{cap: 6 + (images.len * 14)} |
| 208 | append_le_u16(mut data, 0) |
| 209 | append_le_u16(mut data, 1) |
| 210 | append_le_u16(mut data, u16(images.len)) |
| 211 | for i, image in images { |
| 212 | data << image.width |
| 213 | data << image.height |
| 214 | data << image.color_count |
| 215 | data << u8(0) |
| 216 | append_le_u16(mut data, image.planes) |
| 217 | append_le_u16(mut data, image.bit_count) |
| 218 | append_le_u32(mut data, image.bytes_in_res) |
| 219 | append_le_u16(mut data, u16(i + 1)) |
| 220 | } |
| 221 | return data |
| 222 | } |
| 223 | |
| 224 | fn append_le_u16(mut data []u8, value u16) { |
| 225 | data << u8(value & 0xff) |
| 226 | data << u8(value >> 8) |
| 227 | } |
| 228 | |
| 229 | fn append_le_u32(mut data []u8, value u32) { |
| 230 | data << u8(value & 0xff) |
| 231 | data << u8((value >> 8) & 0xff) |
| 232 | data << u8((value >> 16) & 0xff) |
| 233 | data << u8(value >> 24) |
| 234 | } |
| 235 | |
| 236 | @[direct_array_access; inline] |
| 237 | fn read_le_u16(data []u8, offset int) u16 { |
| 238 | return u16(data[offset]) | (u16(data[offset + 1]) << 8) |
| 239 | } |
| 240 | |
| 241 | @[direct_array_access; inline] |
| 242 | fn read_le_u32(data []u8, offset int) u32 { |
| 243 | b0 := u32(data[offset]) |
| 244 | b1 := u32(data[offset + 1]) |
| 245 | b2 := u32(data[offset + 2]) |
| 246 | b3 := u32(data[offset + 3]) |
| 247 | return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24) |
| 248 | } |
| 249 | |
| 250 | @[direct_array_access; inline] |
| 251 | fn read_be_u32(data []u8, offset int) u32 { |
| 252 | b0 := u32(data[offset]) |
| 253 | b1 := u32(data[offset + 1]) |
| 254 | b2 := u32(data[offset + 2]) |
| 255 | b3 := u32(data[offset + 3]) |
| 256 | return (b0 << 24) | (b1 << 16) | (b2 << 8) | b3 |
| 257 | } |
| 258 | |