Skip to content

Commit cc3491b

Browse files
committed
feat: implement icons, closes #8
1 parent 2b7a64c commit cc3491b

7 files changed

Lines changed: 156 additions & 18 deletions

File tree

suap.example.toml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
version = 1
2-
32
# Using Roseate (https://github.com/cloudy-org/roseate) as an example.
4-
display-name = "Roseate" # TODO: add support for display name
3+
4+
# If display name is not specified the
5+
# strict lower-case app name will be used instead
6+
display-name = "Roseate"
7+
8+
# The icons folder is where you'll place your icons.
9+
#
10+
# The name of the image files must be specific like so:
11+
# "windows.ico" - windows specific app icon
12+
# "original.png" - when there's no platform specific icon suap falls back to
13+
icons = "./assets/icons"
514

615
[project.cargo]
716
bin-crate = "roseate" # The name of the cargo binary crate to target

suap/commands/packaging/icons.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from typing import Optional
2+
3+
import shutil
4+
import logging
5+
from pathlib import Path
6+
7+
from ...platform_format import PlatformFormat
8+
9+
__all__ = ()
10+
11+
logger = logging.getLogger(__name__)
12+
13+
def get_platform_icon_path(icons_path: Path, platform_format: PlatformFormat) -> Optional[Path]:
14+
"""
15+
Returns `None` if an icon can't be found / doesn't exist. Otherwise a `Path` is returned.
16+
"""
17+
icon_file_name = None
18+
19+
if platform_format & PlatformFormat.LINUX:
20+
icon_file_name = "linux.png"
21+
22+
elif platform_format & PlatformFormat.WINDOWS:
23+
icon_file_name = "windows.ico"
24+
25+
elif platform_format & PlatformFormat.MACOS:
26+
icon_file_name = "macos.icns"
27+
28+
if icon_file_name is not None:
29+
icon_image_path = icons_path.joinpath(icon_file_name)
30+
31+
if icon_image_path.exists():
32+
logger.debug(f"Platform specific icon was found at '{icon_image_path}'.")
33+
34+
return icon_image_path
35+
36+
original_icon_path = icons_path.joinpath("original.png")
37+
38+
if original_icon_path.exists():
39+
logger.warning(
40+
"No platform specific icons were found! Falling " \
41+
f"back to 'original' icon ({original_icon_path})..."
42+
)
43+
44+
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."
48+
)
49+
50+
return original_icon_path
51+
52+
return None
53+
54+
def format_icon_with_project_name(
55+
icon_path: Path,
56+
temp_folder_path: Path,
57+
project_name: str
58+
) -> Path:
59+
logger.debug(
60+
"Formatting icon with project name and " \
61+
"copying it over to the './temp' directory..."
62+
)
63+
64+
temp_folder_path.mkdir(exist_ok = True)
65+
66+
icon_temp_destination_path = temp_folder_path.joinpath(
67+
f"{project_name}{icon_path.suffix}"
68+
)
69+
70+
logger.debug(f"Copying icon to temp directory at '{icon_temp_destination_path}'...")
71+
72+
shutil.copy(icon_path, icon_temp_destination_path)
73+
74+
return icon_temp_destination_path

suap/commands/packaging/nsis.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import shutil
1+
from typing import Optional
2+
23
import logging
34
from typer import Exit
45
from pathlib import Path
@@ -21,7 +22,8 @@ def format_config_and_make_nsis_installer(
2122
binary_suffix: str,
2223
dist_folder_path: Path,
2324
temp_folder_path: Path,
24-
display_name: str,
25+
display_name: Optional[str],
26+
icon_path: Path,
2527
project_data: ProjectData,
2628
):
2729
logger.debug(
@@ -51,9 +53,11 @@ def format_config_and_make_nsis_installer(
5153

5254
replace_map: dict[str, str] = {
5355
"suap-binary-name": binary_name,
54-
"suap-binary-path": f"../{binary_path}",
55-
"suap-binary-dist-path": str(binary_dist_path),
56-
"suap-display-name": display_name,
56+
"suap-binary-path": str(binary_path.absolute()),
57+
"suap-binary-dist-path": str(binary_dist_path.absolute()),
58+
"suap-display-name": display_name if display_name is not None else project_data.name,
59+
"suap-icon-path": str(icon_path.absolute()),
60+
"suap-icon-file-name": icon_path.name,
5761

5862
"suap-project-name": project_data.name,
5963
"suap-project-version": f"{semver.major}.{semver.minor}.{semver.patch}" \
@@ -74,8 +78,4 @@ def format_config_and_make_nsis_installer(
7478
if not make_nsis_installer(nsis_installer_script_path):
7579
raise Exit(1)
7680

77-
logger.debug("Done making NSIS installer, installer executable should be in dist.")
78-
79-
logger.debug("Removing temp dir...")
80-
81-
shutil.rmtree(temp_folder_path)
81+
logger.info("Done making NSIS installer, installer executable should be in dist.")

suap/commands/packaging/package.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
from ...config.data import ConfigProjectData
55

66
import os
7+
import shutil
78
import logging
89
from pathlib import Path
910
from typer import Option, Typer, Exit
1011

1112
from .binary import format_and_copy_binary_to_dist
1213
from .nsis import format_config_and_make_nsis_installer
14+
from .icons import get_platform_icon_path, format_icon_with_project_name
1315

1416
from ...config import get_config_data
1517
from ...project_type import ProjectType
@@ -29,14 +31,14 @@ def package(
2931
Option(
3032
help = "The packaging format or platform we are targeting. Choose a less generic option if you only want to " \
3133
"package towards ONE specific format such as 'windows-setup', otherwise all available formats " \
32-
"of such platform will be automatically packaged."
34+
"of such platform will be automatically packaged (as is the case with 'windows')."
3335
)
3436
],
3537
bin_output_name: Annotated[
3638
Optional[str],
3739
Option(
3840
help = "Override the default binary name prefixed in front of the binary suffix " \
39-
"(e.g: 'bin-name-linux-x86_64', 'bib-name-win-x86_64-setup.exe', 'bin-name-macos-x86_64')."
41+
"(e.g: 'bin-name-linux-x86_64', 'bin-name-win-x86_64-setup.exe', 'bin-name-macos-x86_64')."
4042
)
4143
] = None,
4244
):
@@ -54,6 +56,31 @@ def package(
5456
raise Exit(1)
5557

5658
display_name: Optional[str] = config_data.get("display-name", None)
59+
icons_config_path: Optional[str] = config_data.get("icons", None)
60+
61+
if icons_config_path is None:
62+
logger.error(
63+
"Icons folder is required to be specific in the config and at " \
64+
"least an original icon must be present (e.g: 'original.png')!" \
65+
'\n icons = "./assets/icons"'
66+
)
67+
raise Exit(1)
68+
69+
icons_path = Path(icons_config_path)
70+
71+
if not icons_path.exists():
72+
logger.error(f"Icons folder path does not exist ('{icons_path}')!")
73+
74+
raise Exit(1)
75+
76+
platform_icon_path = get_platform_icon_path(icons_path, platform_format)
77+
78+
if platform_icon_path is None:
79+
logger.error(
80+
"At least an original icon is required in your icons " \
81+
f"path at '{icons_config_path}' (e.g: 'original.png')!"
82+
)
83+
raise Exit(1)
5784

5885
if project == ProjectType.CARGO:
5986
projects_data: Optional[ConfigProjectData] = config_data.get("project", None)
@@ -115,14 +142,24 @@ def package(
115142
if platform_format & PlatformFormat.WINDOWS_SETUP:
116143
binary_path = cargo_release_path.joinpath(f"{project_data.name}.exe")
117144

145+
platform_icon_path = format_icon_with_project_name(
146+
platform_icon_path,
147+
temp_folder_path,
148+
project_name = project_data.name
149+
)
150+
118151
format_config_and_make_nsis_installer(
119152
binary_path,
120153
binary_name = binary_name,
121154
binary_suffix = "win-x86_64-setup.exe",
122155
dist_folder_path = dist_folder_path,
123156
temp_folder_path = temp_folder_path,
124-
display_name = display_name if display_name is not None else project_data.name,
157+
display_name = display_name,
158+
icon_path = platform_icon_path,
125159
project_data = project_data
126160
)
127161

128-
logger.info("WIP!")
162+
logger.debug("Removing temp dir...")
163+
shutil.rmtree(temp_folder_path, ignore_errors = True)
164+
165+
logger.info("This command is WIP!")

suap/config/data.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class ConfigProjectData(TypedDict):
1919
{
2020
"version": int,
2121
"display-name": str,
22+
"icons": str,
2223
"project": ConfigProjectData
2324
}
2425
)

suap/projects/cargo/building.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import Optional
22

3+
import os
34
import logging
45
from subprocess import check_call, CalledProcessError
56

@@ -30,6 +31,9 @@ def build_cargo_project(toolchain_name: str, cargo_crate_name: str) -> bool:
3031
logger.debug(f"Invoking 'cargo build' with toolchain '{toolchain_name}'...")
3132

3233
try:
34+
default_env = os.environ.copy()
35+
default_env["RUSTFLAGS"] = "-Awarnings" # hides warnings in console
36+
3337
check_call(
3438
[
3539
"cargo",
@@ -39,7 +43,8 @@ def build_cargo_project(toolchain_name: str, cargo_crate_name: str) -> bool:
3943
cargo_crate_name,
4044
"--target",
4145
toolchain_name
42-
]
46+
],
47+
env = default_env
4348
)
4449

4550
except CalledProcessError as error:

suap/templates/nsis_installer_script.nsi

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
!include "MUI2.nsh"
22

33
Name "{suap-display-name}"
4+
BrandingText "{suap-display-name}"
45
OutFile "{suap-binary-dist-path}"
56
InstallDir "$PROGRAMFILES64\cloudy-org\{suap-project-name}"
67

@@ -11,6 +12,13 @@ VIAddVersionKey "FileVersion" "{suap-project-version}"
1112

1213
SetCompressor /SOLID Lzma
1314

15+
# Defining some MUI specific features, like icons for the installer executables
16+
!define MUI_ICON "{suap-icon-path}"
17+
!define MUI_UNICON "{suap-icon-path}"
18+
19+
!define MUI_ABORTWARNING # good UX
20+
!define MUI_UNABORTWARNING
21+
1422
# The typical installer pages a windows user will expect before installing software.
1523
!insertmacro MUI_PAGE_WELCOME
1624
!insertmacro MUI_PAGE_DIRECTORY
@@ -24,20 +32,24 @@ SetCompressor /SOLID Lzma
2432
Section "MainSection"
2533
# Where we place our application binary file and other files.
2634
SetOutPath "$INSTDIR"
35+
36+
# Where we add files from our environment, such as the application binary to the windows install directory.
2737
File "{suap-binary-path}"
38+
File "{suap-icon-path}"
2839

2940
WriteUninstaller "$INSTDIR\uninstall.exe"
3041

3142
# Windows needs to know our application can be uninstalled via it's uninstall apps settings.
3243
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\{suap-project-name}" "DisplayName" "{suap-display-name}"
3344
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\{suap-project-name}" "UninstallString" "$INSTDIR\uninstall.exe"
3445

35-
CreateShortcut "$DESKTOP\{suap-project-name}.lnk" "$INSTDIR\{suap-binary-name}.exe"
46+
CreateShortcut "$DESKTOP\{suap-project-name}.lnk" "$INSTDIR\{suap-binary-name}.exe" "" "$INSTDIR\{suap-icon-file-name}" 0
3647
SectionEnd
3748

3849
Section "Uninstall"
3950
# Where we remove our application binary file and other files.
4051
Delete "$INSTDIR\{suap-binary-name}.exe"
52+
Delete "$INSTDIR\{suap-icon-file-name}"
4153
Delete "$INSTDIR\uninstall.exe"
4254
RMDir "$INSTDIR"
4355

0 commit comments

Comments
 (0)