Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
- **[NSIS](https://nsis.sourceforge.io/Main_Page)** (for packaging windows installers)
- **[Rust](https://www.rust-lang.org/tools/install)** and **Cargo** (for packaging cargo projects).
- **[x86_64-pc-windows-gnu](https://doc.rust-lang.org/nightly/rustc/platform-support/windows-gnu.html)** (for building windows binaries)
- **[mingw-w64-binutils](https://archlinux.org/packages/extra/x86_64/mingw-w64-binutils/)** (required for embedding windows icons onto executables)

## Packaging Usage

Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ suap = "suap.main:app"
[dependency-groups]
dev = [
"ty>=0.0.23",
"ruff>=0.15.6"
"ruff>=0.15.6",
"pytest>=9.0.2",
]

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

[tool.setuptools.package-data]
suap = ["templates/*"]
suap = ["templates/*"]
3 changes: 2 additions & 1 deletion ruff.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

[lint.per-file-ignores]
"__init__.py" = ["F403"]
"__init__.py" = ["F403"]
"tests/*" = ["E712"]
2 changes: 1 addition & 1 deletion suap/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.1.0-alpha.1"
__version__ = "0.1.0-alpha.2"
5 changes: 2 additions & 3 deletions suap/commands/packaging/checks.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import re
import typer
import logging

from ...projects import ProjectData

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

if any(char.isupper() for char in project_name):
logger.error(
"Project name must be all lowercase!"
)
logger.error("Project name must be all lowercase!")

raise typer.Exit(1)

Expand Down
19 changes: 12 additions & 7 deletions suap/commands/packaging/icons.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import Optional

import typer
import shutil
import logging
from pathlib import Path
Expand All @@ -10,7 +9,7 @@

logger = logging.getLogger(__name__)

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

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

raise typer.Exit(1)

return original_icon_path

return None
logger.error(
"At least an original icon is required in your icons " \
f"path at '{icons_path}' (e.g: 'original.png')!"
)
raise typer.Exit(1)

def format_icon_with_project_name(
icon_path: Path,
Expand Down
150 changes: 72 additions & 78 deletions suap/commands/packaging/nsis.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,9 @@
from typer import Exit
from pathlib import Path

# just so we can get the root path of our library
import suap

from ...mime_type import MimeType
from ...projects import ProjectData
from ...templating import Template, Key
from ...formats import make_nsis_installer

__all__ = (
Expand Down Expand Up @@ -38,56 +36,62 @@ def format_config_and_make_nsis_installer(
temp_folder_path.mkdir(exist_ok = True)
nsis_installer_script_path = temp_folder_path.joinpath("nsis_installer.nsi")

templates_path = Path(suap.__file__).parent.joinpath("templates")
installer_script_template_path = templates_path.joinpath("nsis_installer_script.nsi")
installer_script_template = Template("nsis_installer_script.nsi")

logger.debug(
f"Opening and reading NSIS template installer script at '{installer_script_template_path}'..."
)
semver = project_data.version

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

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

semver = project_data.version
display_name = display_name if display_name is not None else project_data.name

replace_map: dict[str, str] = {
"suap-binary-name": binary_name,
"suap-binary-path": str(binary_path.absolute()),
"suap-binary-dist-path": str(binary_dist_path.absolute()),
"suap-display-name": display_name,
"suap-icon-path": str(icon_path.absolute()),
"suap-icon-file-name": icon_path.name,

"suap-project-name": project_data.name,
"suap-project-version": f"{semver.major}.{semver.minor}.{semver.patch}" \
f".{semver.prerelease.split('.')[-1] if semver.prerelease is not None else 0}",
"suap-project-description": project_data.description,

"suap-app-capabilities-macro": generate_app_capabilities_macro(
mime_types,
templates_path,
binary_name,
display_name,
icon_path,
project_data,
uninstall = False,
),
"suap-app-capabilities-uni-macro": generate_app_capabilities_macro(
mime_types,
templates_path,
binary_name,
display_name,
icon_path,
project_data,
uninstall = True,
),
}

for (suap_key, value) in replace_map.items():
installer_script_string = installer_script_string.replace(f"{{{suap_key}}}", value)
icon_file_name_key = Key("icon-file-name", icon_path.name)

installer_script_string = installer_script_template.format(
keys = (
binary_name_key,
Key("binary-path", str(binary_path.absolute())),
Key("binary-dist-path", str(binary_dist_path.absolute())),

display_name_key,

Key("icon-path", str(icon_path.absolute())),
icon_file_name_key,

Key("project-name", project_data.name),
Key(
"project-version",
f"{semver.major}.{semver.minor}.{semver.patch}" \
f".{semver.prerelease.split('.')[-1] if semver.prerelease is not None else 0}"
),
Key("project-description", project_data.description),

Key(
name = "app-capabilities-macro",
value = generate_app_capabilities_macro(
mime_types,
binary_name_key,
display_name_key,
icon_file_name_key,
project_data,
uninstall = False,
)
),
Key(
name = "app-capabilities-uni-macro",
value = generate_app_capabilities_macro(
mime_types,
binary_name_key,
display_name_key,
icon_file_name_key,
project_data,
uninstall = True,
)
),
)
)

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

def generate_app_capabilities_macro(
mime_types: list[MimeType],
templates_path: Path,
binary_name: str,
display_name: str,
icon_path: Path,
binary_name_key: Key,
display_name_key: Key,
icon_file_name_key: Key,
project_data: ProjectData,
uninstall: bool,
) -> str:
if len(mime_types) == 0:
return ""

app_capabilities_template_path = templates_path.joinpath(
"nsis_app_capabilities_uni_macro.nsi" if uninstall else "nsis_app_capabilities_macro.nsi"
)
template_name = "nsis_app_capabilities_macro.nsi"

if uninstall:
template_name = "nsis_app_capabilities_uni_macro.nsi"

with open(app_capabilities_template_path, mode = "r") as file:
app_capabilities_macro_string = file.read()
app_capabilities_template = Template(template_name)

file_associations_and_types_lines = []

Expand All @@ -133,7 +136,7 @@ def generate_app_capabilities_macro(

if not uninstall:
file_associations_and_types_lines.append(
f'WriteRegStr HKCR "Applications\\{binary_name}.exe\\SupportedTypes" "{file_extension}" ""'
f'WriteRegStr HKCR "Applications\\{binary_name_key.name}.exe\\SupportedTypes" "{file_extension}" ""'
)

file_associations_and_types_lines.append(
Expand All @@ -146,35 +149,26 @@ def generate_app_capabilities_macro(

else:
file_associations_and_types_lines.append(
f'DeleteRegValue HKCR "Applications\\{binary_name}.exe\\SupportedTypes" "{file_extension}"'
f'DeleteRegValue HKCR "Applications\\{binary_name_key.name}.exe\\SupportedTypes" "{file_extension}"'
)

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

replace_map: dict[str, str] = {
"suap-binary-name": binary_name,
"suap-display-name": display_name,
"suap-icon-file-name": icon_path.name,

"suap-project-name": project_data.name,
"suap-project-description": project_data.description,

"suap-file-associations-and-types-macro": "\n".join(file_associations_and_types_lines),
}

for (suap_key, value) in replace_map.items():
app_capabilities_macro_string = app_capabilities_macro_string.replace(
f"{{{suap_key}}}", value
app_capabilities_macro_string = app_capabilities_template.format(
keys = (
binary_name_key,
display_name_key,
icon_file_name_key,
Key("project-name", project_data.name),
Key("project-description", project_data.description),
Key("file-associations-and-types-macro", value = "\n".join(file_associations_and_types_lines)),
)
)

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

logger.debug(
f"Generated and formatted app capabilities macro: \n{formatted_macro_string}"
)

return formatted_macro_string
16 changes: 8 additions & 8 deletions suap/commands/packaging/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ def package(
)
raise Exit(1)

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

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

platform_icon_path = get_platform_icon_path(icons_path, platform_format)

if platform_icon_path is None:
logger.error(
"At least an original icon is required in your icons " \
f"path at '{icons_config_path}' (e.g: 'original.png')!"
)
raise Exit(1)

if project == ProjectType.CARGO:
projects_config_data: Optional[ConfigProjectData] = config_data.get("project", None)

Expand Down Expand Up @@ -121,7 +116,12 @@ def package(
)
raise Exit(1)

if not build_cargo_project(toolchain_name, project_data.name):
if not build_cargo_project(
toolchain_name,
project_data.name,
platform_icon_path,
temp_folder_path
):
raise Exit(1)

cargo_release_path = Path(f"./target/{toolchain_name}/release")
Expand Down
16 changes: 16 additions & 0 deletions suap/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
__all__ = ()

class SuapError(Exception):
message: str

def __init__(self, message: str):
self.message = message

super().__init__(message)

class TemplateKeysRemainingError(SuapError):
def __init__(self, remaining_keys: set[str]):
super().__init__(
"There are keys defined in the template " \
f"that were not formatted! Remaining Keys: {remaining_keys}"
)
Loading