Skip to content

Commit 6d95a38

Browse files
authored
Merge pull request #15 from cloudy-org/refactor/switch-to-templating-class
Switch to templating class and add support for embed windows executable icons
2 parents dbfcf63 + ca4e767 commit 6d95a38

14 files changed

Lines changed: 313 additions & 104 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
- **[NSIS](https://nsis.sourceforge.io/Main_Page)** (for packaging windows installers)
1717
- **[Rust](https://www.rust-lang.org/tools/install)** and **Cargo** (for packaging cargo projects).
1818
- **[x86_64-pc-windows-gnu](https://doc.rust-lang.org/nightly/rustc/platform-support/windows-gnu.html)** (for building windows binaries)
19+
- **[mingw-w64-binutils](https://archlinux.org/packages/extra/x86_64/mingw-w64-binutils/)** (required for embedding windows icons onto executables)
1920

2021
## Packaging Usage
2122

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/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.1.0-alpha.1"
1+
__version__ = "0.1.0-alpha.2"

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: 72 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@
44
from typer import Exit
55
from pathlib import Path
66

7-
# just so we can get the root path of our library
8-
import suap
9-
107
from ...mime_type import MimeType
118
from ...projects import ProjectData
9+
from ...templating import Template, Key
1210
from ...formats import make_nsis_installer
1311

1412
__all__ = (
@@ -38,56 +36,62 @@ def format_config_and_make_nsis_installer(
3836
temp_folder_path.mkdir(exist_ok = True)
3937
nsis_installer_script_path = temp_folder_path.joinpath("nsis_installer.nsi")
4038

41-
templates_path = Path(suap.__file__).parent.joinpath("templates")
42-
installer_script_template_path = templates_path.joinpath("nsis_installer_script.nsi")
39+
installer_script_template = Template("nsis_installer_script.nsi")
4340

44-
logger.debug(
45-
f"Opening and reading NSIS template installer script at '{installer_script_template_path}'..."
46-
)
41+
semver = project_data.version
4742

48-
with open(installer_script_template_path, mode = "r") as file:
49-
installer_script_string = file.read()
43+
binary_name_key = Key("binary-name", binary_name)
5044

51-
logger.debug("Formatting template and creating custom install script...")
45+
display_name_key = Key(
46+
name = "display-name",
47+
value = display_name if display_name is not None else project_data.name
48+
)
5249

53-
semver = project_data.version
54-
display_name = display_name if display_name is not None else project_data.name
55-
56-
replace_map: dict[str, str] = {
57-
"suap-binary-name": binary_name,
58-
"suap-binary-path": str(binary_path.absolute()),
59-
"suap-binary-dist-path": str(binary_dist_path.absolute()),
60-
"suap-display-name": display_name,
61-
"suap-icon-path": str(icon_path.absolute()),
62-
"suap-icon-file-name": icon_path.name,
63-
64-
"suap-project-name": project_data.name,
65-
"suap-project-version": f"{semver.major}.{semver.minor}.{semver.patch}" \
66-
f".{semver.prerelease.split('.')[-1] if semver.prerelease is not None else 0}",
67-
"suap-project-description": project_data.description,
68-
69-
"suap-app-capabilities-macro": generate_app_capabilities_macro(
70-
mime_types,
71-
templates_path,
72-
binary_name,
73-
display_name,
74-
icon_path,
75-
project_data,
76-
uninstall = False,
77-
),
78-
"suap-app-capabilities-uni-macro": generate_app_capabilities_macro(
79-
mime_types,
80-
templates_path,
81-
binary_name,
82-
display_name,
83-
icon_path,
84-
project_data,
85-
uninstall = True,
86-
),
87-
}
88-
89-
for (suap_key, value) in replace_map.items():
90-
installer_script_string = installer_script_string.replace(f"{{{suap_key}}}", value)
50+
icon_file_name_key = Key("icon-file-name", icon_path.name)
51+
52+
installer_script_string = installer_script_template.format(
53+
keys = (
54+
binary_name_key,
55+
Key("binary-path", str(binary_path.absolute())),
56+
Key("binary-dist-path", str(binary_dist_path.absolute())),
57+
58+
display_name_key,
59+
60+
Key("icon-path", str(icon_path.absolute())),
61+
icon_file_name_key,
62+
63+
Key("project-name", project_data.name),
64+
Key(
65+
"project-version",
66+
f"{semver.major}.{semver.minor}.{semver.patch}" \
67+
f".{semver.prerelease.split('.')[-1] if semver.prerelease is not None else 0}"
68+
),
69+
Key("project-description", project_data.description),
70+
71+
Key(
72+
name = "app-capabilities-macro",
73+
value = generate_app_capabilities_macro(
74+
mime_types,
75+
binary_name_key,
76+
display_name_key,
77+
icon_file_name_key,
78+
project_data,
79+
uninstall = False,
80+
)
81+
),
82+
Key(
83+
name = "app-capabilities-uni-macro",
84+
value = generate_app_capabilities_macro(
85+
mime_types,
86+
binary_name_key,
87+
display_name_key,
88+
icon_file_name_key,
89+
project_data,
90+
uninstall = True,
91+
)
92+
),
93+
)
94+
)
9195

9296
logger.debug(
9397
f"Creating and writing to custom NSIS installer script at '{nsis_installer_script_path}'..."
@@ -103,22 +107,21 @@ def format_config_and_make_nsis_installer(
103107

104108
def generate_app_capabilities_macro(
105109
mime_types: list[MimeType],
106-
templates_path: Path,
107-
binary_name: str,
108-
display_name: str,
109-
icon_path: Path,
110+
binary_name_key: Key,
111+
display_name_key: Key,
112+
icon_file_name_key: Key,
110113
project_data: ProjectData,
111114
uninstall: bool,
112115
) -> str:
113116
if len(mime_types) == 0:
114117
return ""
115118

116-
app_capabilities_template_path = templates_path.joinpath(
117-
"nsis_app_capabilities_uni_macro.nsi" if uninstall else "nsis_app_capabilities_macro.nsi"
118-
)
119+
template_name = "nsis_app_capabilities_macro.nsi"
120+
121+
if uninstall:
122+
template_name = "nsis_app_capabilities_uni_macro.nsi"
119123

120-
with open(app_capabilities_template_path, mode = "r") as file:
121-
app_capabilities_macro_string = file.read()
124+
app_capabilities_template = Template(template_name)
122125

123126
file_associations_and_types_lines = []
124127

@@ -133,7 +136,7 @@ def generate_app_capabilities_macro(
133136

134137
if not uninstall:
135138
file_associations_and_types_lines.append(
136-
f'WriteRegStr HKCR "Applications\\{binary_name}.exe\\SupportedTypes" "{file_extension}" ""'
139+
f'WriteRegStr HKCR "Applications\\{binary_name_key.name}.exe\\SupportedTypes" "{file_extension}" ""'
137140
)
138141

139142
file_associations_and_types_lines.append(
@@ -146,35 +149,26 @@ def generate_app_capabilities_macro(
146149

147150
else:
148151
file_associations_and_types_lines.append(
149-
f'DeleteRegValue HKCR "Applications\\{binary_name}.exe\\SupportedTypes" "{file_extension}"'
152+
f'DeleteRegValue HKCR "Applications\\{binary_name_key.name}.exe\\SupportedTypes" "{file_extension}"'
150153
)
151154

152155
file_associations_and_types_lines.append(
153156
f'DeleteRegValue HKCR "{file_extension}\\OpenWithProgIds" "Cloudy.{project_name}.1"'
154157
)
155158

156-
replace_map: dict[str, str] = {
157-
"suap-binary-name": binary_name,
158-
"suap-display-name": display_name,
159-
"suap-icon-file-name": icon_path.name,
160-
161-
"suap-project-name": project_data.name,
162-
"suap-project-description": project_data.description,
163-
164-
"suap-file-associations-and-types-macro": "\n".join(file_associations_and_types_lines),
165-
}
166-
167-
for (suap_key, value) in replace_map.items():
168-
app_capabilities_macro_string = app_capabilities_macro_string.replace(
169-
f"{{{suap_key}}}", value
159+
app_capabilities_macro_string = app_capabilities_template.format(
160+
keys = (
161+
binary_name_key,
162+
display_name_key,
163+
icon_file_name_key,
164+
Key("project-name", project_data.name),
165+
Key("project-description", project_data.description),
166+
Key("file-associations-and-types-macro", value = "\n".join(file_associations_and_types_lines)),
170167
)
168+
)
171169

172170
formatted_macro_string = "".join([
173171
f" {line}" for line in app_capabilities_macro_string.splitlines(keepends = True)
174172
])
175173

176-
logger.debug(
177-
f"Generated and formatted app capabilities macro: \n{formatted_macro_string}"
178-
)
179-
180174
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+
)

0 commit comments

Comments
 (0)