Skip to content

Commit a90f7d8

Browse files
committed
feat: add windows icon embedding on executables and add testing for templating
1 parent f9c4eb9 commit a90f7d8

12 files changed

Lines changed: 193 additions & 50 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ suap = "suap.main:app"
1515
[dependency-groups]
1616
dev = [
1717
"ty>=0.0.23",
18-
"ruff>=0.15.6"
18+
"ruff>=0.15.6",
19+
"pytest>=9.0.2",
1920
]
2021

2122
[build-system]
@@ -29,4 +30,4 @@ version = { attr = "suap.__version__" }
2930
include = ["suap*"]
3031

3132
[tool.setuptools.package-data]
32-
suap = ["templates/*"]
33+
suap = ["templates/*"]

ruff.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11

22
[lint.per-file-ignores]
3-
"__init__.py" = ["F403"]
3+
"__init__.py" = ["F403"]
4+
"tests/*" = ["E712"]

suap/commands/packaging/checks.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import re
22
import typer
33
import logging
4+
45
from ...projects import ProjectData
56

67
__all__ = ()
@@ -23,9 +24,7 @@ def check_project_data_validity(project_data: ProjectData) -> None:
2324
raise typer.Exit(1)
2425

2526
if any(char.isupper() for char in project_name):
26-
logger.error(
27-
"Project name must be all lowercase!"
28-
)
27+
logger.error("Project name must be all lowercase!")
2928

3029
raise typer.Exit(1)
3130

suap/commands/packaging/icons.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from typing import Optional
2-
1+
import typer
32
import shutil
43
import logging
54
from pathlib import Path
@@ -10,7 +9,7 @@
109

1110
logger = logging.getLogger(__name__)
1211

13-
def get_platform_icon_path(icons_path: Path, platform_format: PlatformFormat) -> Optional[Path]:
12+
def get_platform_icon_path(icons_path: Path, platform_format: PlatformFormat) -> Path:
1413
"""
1514
Returns `None` if an icon can't be found / doesn't exist. Otherwise a `Path` is returned.
1615
"""
@@ -42,14 +41,20 @@ def get_platform_icon_path(icons_path: Path, platform_format: PlatformFormat) ->
4241
)
4342

4443
if platform_format & PlatformFormat.WINDOWS:
45-
logger.warning(
46-
"You will MOST LIKELY want a platform specific icon for Windows "\
47-
"(e.g: 'windows.ico') as support for PNGs are a gray area and can cause problems."
44+
logger.error(
45+
"Platform specific icon is REQUIRED for WINDOWS platform! A " \
46+
f"'windows.ico' file must be provided in your icons folder ({icons_path})."
4847
)
4948

49+
raise typer.Exit(1)
50+
5051
return original_icon_path
5152

52-
return None
53+
logger.error(
54+
"At least an original icon is required in your icons " \
55+
f"path at '{icons_path}' (e.g: 'original.png')!"
56+
)
57+
raise typer.Exit(1)
5358

5459
def format_icon_with_project_name(
5560
icon_path: Path,

suap/commands/packaging/nsis.py

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66

77
from ...mime_type import MimeType
88
from ...projects import ProjectData
9-
from ...formats import make_nsis_installer
109
from ...templating import Template, Key
10+
from ...formats import make_nsis_installer
1111

1212
__all__ = (
1313
"format_config_and_make_nsis_installer",
@@ -40,33 +40,33 @@ def format_config_and_make_nsis_installer(
4040

4141
semver = project_data.version
4242

43-
binary_name_key = Key(name = "binary-name", value = binary_name)
43+
binary_name_key = Key("binary-name", binary_name)
4444

4545
display_name_key = Key(
4646
name = "display-name",
4747
value = display_name if display_name is not None else project_data.name
4848
)
4949

50-
icon_file_name_key = Key(name = "icon-file-name", value = icon_path.name)
50+
icon_file_name_key = Key("icon-file-name", icon_path.name)
5151

5252
installer_script_string = installer_script_template.format(
5353
keys = (
5454
binary_name_key,
55-
Key(name = "binary-path", value = str(binary_path.absolute())),
56-
Key(name = "binary-dist-path", value = str(binary_dist_path.absolute())),
55+
Key("binary-path", str(binary_path.absolute())),
56+
Key("binary-dist-path", str(binary_dist_path.absolute())),
5757

5858
display_name_key,
5959

60-
Key(name = "icon-path", value = str(icon_path.absolute())),
60+
Key("icon-path", str(icon_path.absolute())),
6161
icon_file_name_key,
6262

63-
Key(name = "project-name", value = project_data.name),
63+
Key("project-name", project_data.name),
6464
Key(
65-
name = "project-version",
66-
value = f"{semver.major}.{semver.minor}.{semver.patch}" \
65+
"project-version",
66+
f"{semver.major}.{semver.minor}.{semver.patch}" \
6767
f".{semver.prerelease.split('.')[-1] if semver.prerelease is not None else 0}"
6868
),
69-
Key(name = "project-description", value = project_data.description),
69+
Key("project-description", project_data.description),
7070

7171
Key(
7272
name = "app-capabilities-macro",
@@ -171,8 +171,4 @@ def generate_app_capabilities_macro(
171171
f" {line}" for line in app_capabilities_macro_string.splitlines(keepends = True)
172172
])
173173

174-
logger.debug(
175-
f"Generated and formatted app capabilities macro: \n{formatted_macro_string}"
176-
)
177-
178174
return formatted_macro_string

suap/commands/packaging/package.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ def package(
7373
)
7474
raise Exit(1)
7575

76+
# TODO: move this and other processed and
77+
# checked data into some sort of object / class
7678
icons_path = Path(icons_config_path)
7779

7880
if not icons_path.exists():
@@ -82,13 +84,6 @@ def package(
8284

8385
platform_icon_path = get_platform_icon_path(icons_path, platform_format)
8486

85-
if platform_icon_path is None:
86-
logger.error(
87-
"At least an original icon is required in your icons " \
88-
f"path at '{icons_config_path}' (e.g: 'original.png')!"
89-
)
90-
raise Exit(1)
91-
9287
if project == ProjectType.CARGO:
9388
projects_config_data: Optional[ConfigProjectData] = config_data.get("project", None)
9489

@@ -121,7 +116,12 @@ def package(
121116
)
122117
raise Exit(1)
123118

124-
if not build_cargo_project(toolchain_name, project_data.name):
119+
if not build_cargo_project(
120+
toolchain_name,
121+
project_data.name,
122+
platform_icon_path,
123+
temp_folder_path
124+
):
125125
raise Exit(1)
126126

127127
cargo_release_path = Path(f"./target/{toolchain_name}/release")

suap/errors.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
__all__ = ()
2+
3+
class SuapError(Exception):
4+
message: str
5+
6+
def __init__(self, message: str):
7+
self.message = message
8+
9+
super().__init__(message)
10+
11+
class TemplateKeysRemainingError(SuapError):
12+
def __init__(self, remaining_keys: set[str]):
13+
super().__init__(
14+
"There are keys defined in the template " \
15+
f"that were not formatted! Remaining Keys: {remaining_keys}"
16+
)

suap/projects/cargo/building.py

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import os
44
import logging
5+
from pathlib import Path
56
from subprocess import check_call, CalledProcessError
67

78
from ...platform_format import PlatformFormat
@@ -28,15 +29,28 @@ def get_cargo_toolchain(platform_format: PlatformFormat) -> Optional[str]:
2829

2930
return toolchain_name
3031

31-
def build_cargo_project(toolchain_name: str, cargo_crate_name: str) -> bool:
32+
def build_cargo_project(
33+
toolchain_name: str,
34+
cargo_crate_name: str,
35+
icon_path: Path,
36+
temp_folder_path: Path
37+
) -> bool:
3238
logger.debug(f"Invoking 'cargo build' with toolchain '{toolchain_name}'...")
3339

3440
try:
3541
default_env = os.environ.copy()
36-
default_env["RUSTFLAGS"] = "-Awarnings" # hides warnings in console
42+
43+
rust_flags = ["-Awarnings"] # hides warnings in console
44+
45+
if toolchain_name == "x86_64-pc-windows-gnu":
46+
compiled_resource_path = build_windows_resource_file(icon_path, temp_folder_path)
47+
48+
rust_flags.extend(["-C", f"link-arg={compiled_resource_path}"])
49+
50+
default_env["RUSTFLAGS"] = " ".join(rust_flags)
3751

3852
logger.warning(
39-
f"RUSTFLAGS are not inherited! 'RUSTFLAGS' will contain -> '{default_env['RUSTFLAGS']}'."
53+
f"RUSTFLAGS is NOT inherited! 'RUSTFLAGS' will contain -> '{default_env['RUSTFLAGS']}'."
4054
)
4155

4256
check_call(
@@ -59,4 +73,42 @@ def build_cargo_project(toolchain_name: str, cargo_crate_name: str) -> bool:
5973

6074
return False
6175

62-
return True
76+
return True
77+
78+
def build_windows_resource_file(icon_path: Path, temp_folder_path: Path) -> Optional[Path]:
79+
logger.debug("Building windows resource file...")
80+
81+
resource_file_path = temp_folder_path.joinpath("resource.rc")
82+
compiled_resource_binary_path = temp_folder_path.joinpath("resource.res")
83+
84+
resource_contents = f"""
85+
IDI_MYICON ICON "{icon_path.absolute()}"
86+
"""
87+
88+
with open(resource_file_path, mode = "w") as file:
89+
file.write(resource_contents)
90+
91+
try:
92+
logger.debug(f"Invoking 'x86_64-w64-mingw32-windres' to build '{compiled_resource_binary_path}'...")
93+
94+
check_call(
95+
# now if you're using a distro that doesn't name
96+
# windres as "x86_64-w64-mingw32-windres"... *Eh... your loss...*
97+
args = [
98+
"x86_64-w64-mingw32-windres",
99+
f"{resource_file_path.absolute()}",
100+
"-O",
101+
"coff",
102+
"-o",
103+
f"{compiled_resource_binary_path.absolute()}"
104+
]
105+
)
106+
107+
except CalledProcessError as error:
108+
logger.error(
109+
f"Failed to build windows resource file (resource.res)! Error: {error}"
110+
)
111+
112+
return None
113+
114+
return compiled_resource_binary_path

suap/templates/test_template.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
The kitty goes... {suap-insert-meow}! Some weird formatting... {{suap-some-weird-format-1}" {{suap-some-weird-format-1}}} {{suap-some-weird-format-2}} suap-umm

suap/templating.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from pathlib import Path
44
from dataclasses import dataclass
55

6+
from .errors import TemplateKeysRemainingError
7+
68
__all__ = ()
79

810
logger = logging.getLogger(__name__)
@@ -12,13 +14,6 @@ class Key:
1214
name: str
1315
value: str
1416

15-
class TemplateKeysRemainingError(Exception):
16-
def __init__(self, remaining_keys: set[str]):
17-
super().__init__(
18-
"There are keys defined in the template " \
19-
f"that were not formatted! Remaining Keys: {remaining_keys}"
20-
)
21-
2217
class Template():
2318
def __init__(self, template_name: str):
2419
self.__template_string = self.__get_template_contents(template_name)
@@ -30,23 +25,22 @@ def __init__(self, template_name: str):
3025

3126
self.__defined_suap_keys: set[str] = set(defined_suap_keys_list)
3227

33-
print(f"--> {self.__defined_suap_keys}")
34-
3528
def format(self, keys: tuple[Key, ...]) -> str:
3629
formatted_string = self.__template_string
3730

3831
remaining_keys = self.__defined_suap_keys
3932

4033
for key in keys:
4134
key_name = key.name
35+
key_value = str(key.value)
4236

4337
if key_name in remaining_keys:
44-
logger.debug(f"Formatting template key '{key_name}'...")
38+
logger.debug(f"Formatting template key '{key_name}' with value '{key_value}'...")
4539

4640
remaining_keys.remove(key_name)
4741

4842
formatted_string = formatted_string.replace(
49-
f"{{suap-{key_name}}}", str(key.value)
43+
f"{{suap-{key_name}}}", key_value
5044
)
5145

5246
if len(remaining_keys) > 0:

0 commit comments

Comments
 (0)