feat: restructure output layout per platform and add MSIX artifact support

This commit is contained in:
yyc12345 committed 2026-10-15 13:42:33 +08:00
1 parent 7f3a281891
commit d06b1835b6
9 files changed
+277 -70

No files matched your search

+5 -1
View File
@@ -6,9 +6,13 @@ class AddressDomain(enum.IntEnum):
Temporary = enum.auto()
Output = enum.auto()
FdOutput = enum.auto()
PosixOutput = enum.auto()
PosixFdOutput = enum.auto()
WinOutput = enum.auto()
WinIcoOutput = enum.auto()
WinMsixOutput = enum.auto()
MacOutput = enum.auto()
MacIcnsOutput = enum.auto()
class Address:
+199 -41
View File
@@ -7,7 +7,17 @@ from pathlib import Path
from typing import TypeVar, Generic, Callable
from ..lowlevel.environment import Prologue as LowPrologue, Environment as LowEnv
from ..lowlevel.artrdr import SvgRenderKind
from .utils import FdContext, WinCategory, MacCategory, VART_HW
from .utils import (
FdContext,
IcoCategory,
MsixCategory,
MsixAppLogoKind,
MsixAppPlatedKind,
IcnsCategory,
check_fd_hw,
check_msix_scale,
check_msix_targetsize,
)
@dataclass(frozen=True)
@@ -30,11 +40,7 @@ class Prologue(LowPrologue):
def __post_init__(self) -> None:
super().__post_init__()
for hw in self.fd_thumbnail_hws:
if hw <= 0 or hw > VART_HW:
raise ValueError(
f"Given FreeDesktop icon thumbnail size mismatch the size requirements. "
f"Expect >= 0 and <= {VART_HW}, got {hw}."
)
check_fd_hw(hw)
_TEMP_USER_DIR: str = "user"
@@ -42,8 +48,13 @@ _TEMP_LIB_DIR: str = "assemblicon"
_TEMP_LIB_ALLOC_DIR: str = "alloc"
_TEMP_LIB_STABLE_DIR: str = "stable"
_ART_POSIX: str = "posix"
_ART_POSIX_FD: str = "freedesktop"
_ART_WIN: str = "windows"
_ART_WIN_ICO: str = "ico"
_ART_WIN_MSIX: str = "msix"
_ART_MAC: str = "macos"
_ART_MAC_ICNS: str = "icns"
P = TypeVar("P", bound=Prologue)
@@ -55,36 +66,11 @@ class Environment(Generic[P]):
"""The lowlevel environment managing temporary directory."""
def __init__(self, prologue: P) -> None:
# assign members
self.__prologue = prologue
self.__lowenv = LowEnv(self.__prologue)
# construct basic layout of temporary directory
self.__temporary_artifact(_TEMP_USER_DIR).mkdir(parents=True, exist_ok=True)
self.__temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_ALLOC_DIR).mkdir(
parents=True, exist_ok=True
)
self.__temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_STABLE_DIR).mkdir(
parents=True, exist_ok=True
)
# and output directory
for hw in self.__prologue.fd_thumbnail_hws:
for kind in FdContext:
self.__output_artifact(f"{hw}x{hw}", str(kind)).mkdir(
parents=True, exist_ok=True
)
for kind in WinCategory:
self.__output_artifact(_ART_WIN, str(kind)).mkdir(
parents=True, exist_ok=True
)
for kind in MacCategory:
self.__output_artifact(_ART_MAC, str(kind)).mkdir(
parents=True, exist_ok=True
)
# construct user custom temporary layout of possible
if self.__prologue.temp_dir_initializer is not None:
temp_dir = self.user_temporary_artifact()
self.__prologue.temp_dir_initializer(temp_dir)
# build directory hierarchy
self.__build_dir_hierarchy()
# region: With Visitor Requirements
@@ -102,6 +88,64 @@ class Environment(Generic[P]):
# endregion
# region: Directory Hierarchy Builder
def __build_dir_hierarchy(self) -> None:
self.__build_temp_dir_hierarchy()
self.__build_output_dir_hierarchy()
def __build_temp_dir_hierarchy(self) -> None:
# construct basic layout of temporary directory
self.__temporary_artifact(_TEMP_USER_DIR).mkdir(parents=True, exist_ok=True)
self.__temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_ALLOC_DIR).mkdir(
parents=True, exist_ok=True
)
self.__temporary_artifact(_TEMP_LIB_DIR, _TEMP_LIB_STABLE_DIR).mkdir(
parents=True, exist_ok=True
)
# construct user custom temporary layout of possible
if self.__prologue.temp_dir_initializer is not None:
temp_dir = self.user_temporary_artifact()
self.__prologue.temp_dir_initializer(temp_dir)
def __build_output_dir_hierarchy(self) -> None:
# build top layout
self.__posix_output_artifact().mkdir(parents=True, exist_ok=True)
self.__win_output_artifact().mkdir(parents=True, exist_ok=True)
self.__mac_output_artifact().mkdir(parents=True, exist_ok=True)
# build sub-layout
self.__posix_output_artifact(_ART_POSIX_FD).mkdir(parents=True, exist_ok=True)
self.__win_output_artifact(_ART_WIN_ICO).mkdir(parents=True, exist_ok=True)
self.__win_output_artifact(_ART_WIN_MSIX).mkdir(parents=True, exist_ok=True)
self.__mac_output_artifact(_ART_MAC_ICNS).mkdir(parents=True, exist_ok=True)
# build the lowest layout
# for POSIX FreeDesktop
for hw in self.__prologue.fd_thumbnail_hws:
for kind in FdContext:
self.__posix_output_artifact(
_ART_POSIX_FD, f"{hw}x{hw}", str(kind)
).mkdir(parents=True, exist_ok=True)
# for Windows ICO
for kind in IcoCategory:
self.__win_output_artifact(_ART_WIN_ICO, str(kind)).mkdir(
parents=True, exist_ok=True
)
# for Windows MSIX
for kind in MsixCategory:
self.__win_output_artifact(_ART_WIN_MSIX, str(kind)).mkdir(
parents=True, exist_ok=True
)
# for macOS ICNS
for kind in IcnsCategory:
self.__mac_output_artifact(_ART_MAC_ICNS, str(kind)).mkdir(
parents=True, exist_ok=True
)
# endregion
# region: Prologue Visitor
@property
@@ -176,6 +220,24 @@ class Environment(Generic[P]):
"""
return self.__lowenv.output_artifact(*args)
def __posix_output_artifact(self, *args: str) -> Path:
"""
Get the path to output POSIX artifacts.
"""
return self.__output_artifact(_ART_POSIX, *args)
def __win_output_artifact(self, *args: str) -> Path:
"""
Get the path to output Windows artifacts.
"""
return self.__output_artifact(_ART_WIN, *args)
def __mac_output_artifact(self, *args: str) -> Path:
"""
Get the path to output macOS artifacts.
"""
return self.__output_artifact(_ART_MAC, *args)
def fd_output_artifact(self, hw: int, kind: FdContext, name: str) -> Path:
"""
Get the path for saving FreeDesktop icon with given image size, FreeDesktop icon and name.
@@ -184,25 +246,121 @@ class Environment(Generic[P]):
Required because FreeDesktop need to save icon with different size variant.
:param kind: The FreeDesktop kind of this icon.
:param name: The name of saved artifact. The file extension ".png" is not required.
:returns: The path to saved artifact.
"""
return self.__output_artifact(f"{hw}x{hw}", str(kind), f"{name}.png")
check_fd_hw(hw)
return self.__posix_output_artifact(
_ART_POSIX_FD, f"{hw}x{hw}", str(kind), f"{name}.png"
)
def win_output_artifact(self, kind: WinCategory, name: str) -> Path:
def ico_output_artifact(self, kind: IcoCategory, name: str) -> Path:
"""
Get the path for saving Windows-only icon.
Get the path for saving Windows ICO icon.
:param kind: The kind of this icon.
:param name: The name of saved artifact. The file extension ".ico" is not required.
:returns: The path to saved artifact.
"""
return self.__output_artifact(_ART_WIN, str(kind), f"{name}.ico")
return self.__win_output_artifact(_ART_WIN_ICO, str(kind), f"{name}.ico")
def mac_output_artifact(self, kind: MacCategory, name: str) -> Path:
def __msix_output_artifact(self, kind: MsixCategory, *args: str) -> Path:
"""
Get the path for saving macOS-only icon.
Get the path for saving Windows MSIX icon.
:param kind: The category of this icon.
:returns: The path to saved artifact.
"""
return self.__win_output_artifact(_ART_WIN_MSIX, str(kind), *args)
def msix_app_output_artifact(
self,
kind: MsixAppLogoKind,
scale: Optional[int],
targetsize: Optional[int],
plated: Optional[MsixAppPlatedKind],
name: str,
) -> Path:
"""
Get the path for saving Windows MSIX application logo icon.
Parameter ``scale`` and ``targetsize`` cannot both be non-``None``.
Parameter ``targetsize`` and ``plated`` must be either both ``None`` or both non-``None``.
:param kind: The detailed kind of MSIX application icon.
:param scale: The scale variant of this artifact.
``None`` for no variant suffix.
:param targetsize: The targetsize variant of this artifact.
``None`` for no targetsize suffix or non-``MsixAppLogoKind.Square44x44`` icon.
:param plated: The plated variant of this artifact.
``None`` for non-``MsixAppLogoKind.Square44x44`` icon.
:param name: The name of saved artifact. The file extension ".png" is not required.
:returns: The path to saved artifact.
"""
# check parameter restrictions
if scale is not None and targetsize is not None:
raise ValueError(
f"MSIX doesn't allow the combination of scale and targetsize variant"
)
if kind != MsixAppLogoKind.Square44x44 and targetsize is not None:
raise ValueError(
f"MSIX targetsize variant is not supported on non-Square44x44 logo"
)
if (targetsize is None) ^ (plated is None):
raise ValueError(
f"MSIX plated feature must be provided with targetsize variant"
)
# build file name part
if scale is not None:
check_msix_scale(scale)
filename = f"{name}.{kind}.scale-{scale}.png"
elif targetsize is not None:
check_msix_targetsize(targetsize)
# TODO: This altform code are ugly (do not utilize enum.StrEnum)
# May be improved in future.
altform: Optional[str]
match plated:
case MsixAppPlatedKind.Plated:
altform = None
case MsixAppPlatedKind.Unplated:
altform = "altform-unplated"
case _:
raise ValueError(f"Unknown plated feature: {plated}")
if altform is None:
filename = f"{name}.{kind}.targetsize-{targetsize}.png"
else:
filename = f"{name}.{kind}.targetsize-{targetsize}_{altform}.png"
else:
filename = f"{name}.{kind}.png"
# build result
return self.__msix_output_artifact(MsixCategory.Application, filename)
def msix_ext_output_artifact(self, targetsize: Optional[int], name: str) -> Path:
"""
Get the path for saving Windows MSIX file association icon.
:param targetsize: The targetsize variant of this artifact.
``None`` for no variant suffix.
:param name: The name of saved artifact. The file extension ".png" is not required.
:returns: The path to saved artifact.
"""
if targetsize is None:
filename = f"{name}.png"
else:
check_msix_targetsize(targetsize)
filename = f"{name}.targetsize-{targetsize}.png"
return self.__msix_output_artifact(MsixCategory.Extension, filename)
def icns_output_artifact(self, kind: IcnsCategory, name: str) -> Path:
"""
Get the path for saving macOS ICNS icon.
:param kind: The kind of this icon.
:param name: The name of saved artifact. The file extension ".icns" is not required.
:returns: The path to saved artifact.
"""
return self.__output_artifact(_ART_MAC, str(kind), f"{name}.icns")
return self.__mac_output_artifact(_ART_MAC_ICNS, str(kind), f"{name}.icns")
# endregion
+1 -1
View File
@@ -23,7 +23,7 @@ class FdArtSink(Sink):
@property
def address(self) -> Address:
return Address(AddressDomain.FdOutput, str(self.__kind), self.__name)
return Address(AddressDomain.PosixFdOutput, str(self.__kind), self.__name)
def push(self, env: Environment, art: Artifact) -> None:
LOGGER.info(
@@ -2,28 +2,28 @@ from dataclasses import dataclass
from .common import Sink
from ..address import Address, AddressDomain
from ..environment import Environment
from ..utils import Artifact, MacCategory, check_vart
from ..utils import Artifact, IcnsCategory, check_vart
from ...lowlevel import artrdr
from ...logger import HIGHLEVEL_LOGGER as LOGGER
@dataclass(frozen=True)
class IcnsArtSinkHint:
kind: MacCategory
kind: IcnsCategory
name: str
class IcnsArtSink(Sink):
__kind: MacCategory
__kind: IcnsCategory
__name: str
def __init__(self, kind: MacCategory, name: str) -> None:
def __init__(self, kind: IcnsCategory, name: str) -> None:
self.__kind = kind
self.__name = name
@property
def address(self) -> Address:
return Address(AddressDomain.MacOutput, str(self.__kind), self.__name)
return Address(AddressDomain.MacIcnsOutput, str(self.__kind), self.__name)
def push(self, env: Environment, art: Artifact) -> None:
LOGGER.info(
@@ -32,7 +32,7 @@ class IcnsArtSink(Sink):
self.__name,
)
check_vart(art)
artrdr.render_icns(art, env.mac_output_artifact(self.__kind, self.__name))
artrdr.render_icns(art, env.icns_output_artifact(self.__kind, self.__name))
def push_hint(self) -> IcnsArtSinkHint:
return IcnsArtSinkHint(self.__kind, self.__name)
@@ -2,28 +2,28 @@ from dataclasses import dataclass
from .common import Sink
from ..address import Address, AddressDomain
from ..environment import Environment
from ..utils import Artifact, WinCategory, check_vart
from ..utils import Artifact, IcoCategory, check_vart
from ...lowlevel import artrdr
from ...logger import HIGHLEVEL_LOGGER as LOGGER
@dataclass(frozen=True)
class IcoArtSinkHint:
kind: WinCategory
kind: IcoCategory
name: str
class IcoArtSink(Sink):
__kind: WinCategory
__kind: IcoCategory
__name: str
def __init__(self, kind: WinCategory, name: str) -> None:
def __init__(self, kind: IcoCategory, name: str) -> None:
self.__kind = kind
self.__name = name
@property
def address(self) -> Address:
return Address(AddressDomain.WinOutput, str(self.__kind), self.__name)
return Address(AddressDomain.WinIcoOutput, str(self.__kind), self.__name)
def push(self, env: Environment, art: Artifact) -> None:
LOGGER.info(
@@ -32,7 +32,7 @@ class IcoArtSink(Sink):
self.__name,
)
check_vart(art)
artrdr.render_ico(art, env.win_output_artifact(self.__kind, self.__name))
artrdr.render_ico(art, env.ico_output_artifact(self.__kind, self.__name))
def push_hint(self) -> IcoArtSinkHint:
return IcoArtSinkHint(self.__kind, self.__name)
@@ -17,7 +17,7 @@ class FdArtSource(Source):
@property
def address(self) -> Address:
return Address(AddressDomain.FdOutput, str(self.__kind), self.__name)
return Address(AddressDomain.PosixFdOutput, str(self.__kind), self.__name)
def link_to(self, env: Environment, fd_sink: FdArtSink) -> None:
sink_hint = fd_sink.push_hint()
@@ -2,22 +2,22 @@ from .common import Source
from ..address import Address, AddressDomain
from ..environment import Environment
from ..sink.icnsart_sink import IcnsArtSink
from ..utils import MacCategory
from ..utils import IcnsCategory
from ...lowlevel import artio
from ...logger import HIGHLEVEL_LOGGER as LOGGER
class IcnsArtSource(Source):
__kind: MacCategory
__kind: IcnsCategory
__name: str
def __init__(self, kind: MacCategory, name: str) -> None:
def __init__(self, kind: IcnsCategory, name: str) -> None:
self.__kind = kind
self.__name = name
@property
def address(self) -> Address:
return Address(AddressDomain.MacOutput, str(self.__kind), self.__name)
return Address(AddressDomain.MacIcnsOutput, str(self.__kind), self.__name)
def link_to(self, env: Environment, mac_sink: IcnsArtSink) -> None:
sink_hint = mac_sink.push_hint()
@@ -29,6 +29,6 @@ class IcnsArtSource(Source):
sink_hint.name,
)
artio.link_artifact(
env.mac_output_artifact(self.__kind, self.__name),
env.mac_output_artifact(sink_hint.kind, sink_hint.name),
env.icns_output_artifact(self.__kind, self.__name),
env.icns_output_artifact(sink_hint.kind, sink_hint.name),
)
@@ -2,22 +2,22 @@ from .common import Source
from ..address import Address, AddressDomain
from ..environment import Environment
from ..sink.icoart_sink import IcoArtSink
from ..utils import WinCategory
from ..utils import IcoCategory
from ...lowlevel import artio
from ...logger import HIGHLEVEL_LOGGER as LOGGER
class IcoArtSource(Source):
__kind: WinCategory
__kind: IcoCategory
__name: str
def __init__(self, kind: WinCategory, name: str) -> None:
def __init__(self, kind: IcoCategory, name: str) -> None:
self.__kind = kind
self.__name = name
@property
def address(self) -> Address:
return Address(AddressDomain.WinOutput, str(self.__kind), self.__name)
return Address(AddressDomain.WinIcoOutput, str(self.__kind), self.__name)
def link_to(self, env: Environment, win_sink: IcoArtSink) -> None:
sink_hint = win_sink.push_hint()
@@ -29,6 +29,6 @@ class IcoArtSource(Source):
sink_hint.name,
)
artio.link_artifact(
env.win_output_artifact(self.__kind, self.__name),
env.win_output_artifact(sink_hint.kind, sink_hint.name),
env.ico_output_artifact(self.__kind, self.__name),
env.ico_output_artifact(sink_hint.kind, sink_hint.name),
)
+47 -2
View File
@@ -12,6 +12,26 @@ def check_vart(art: Artifact) -> None:
)
def check_fd_hw(hw: int) -> None:
if hw <= 0 or hw > VART_HW:
raise ValueError(
f"Invalid FreeDesktop icon thumbnail size. "
f"Expect > 0 and <= {VART_HW}, got {hw}."
)
def check_msix_scale(scale: int) -> None:
if scale <= 100:
raise ValueError(f"Invalid scale value for MSIX icon variant: {scale}")
def check_msix_targetsize(targetsize: int) -> None:
if targetsize <= 0:
raise ValueError(
f"Invalid targetsize value for MSIX icon variant: {targetsize}"
)
class FdContext(enum.StrEnum):
Actions = "actions"
Animations = "animations"
@@ -26,14 +46,39 @@ class FdContext(enum.StrEnum):
Status = "status"
class WinCategory(enum.StrEnum):
class IcoCategory(enum.StrEnum):
Application = "apps"
"""Icons for applications"""
Extension = "exts"
"""Icons for file extensions"""
class MacCategory(enum.StrEnum):
class MsixCategory(enum.StrEnum):
Application = "Apps"
"""Icons for application logos"""
Extension = "FileAssocs"
"""Icons for file extensions"""
class MsixAppLogoKind(enum.StrEnum):
# TODO: We may add more kinds in there.
# But current kinds are enough to upload application to store.
StoreLogo = "StoreLogo"
"""The 50x50 icon used for store showcase."""
Square44x44 = "Square44x44Logo"
"""The 44x44 icon used for application list (including taskbar and etc)."""
Square150x150 = "Square150x150Logo"
"""The 150x150 icon used for medium tile."""
class MsixAppPlatedKind(enum.StrEnum):
Plated = "plated"
"""Plated icon (has solid color box in background)"""
Unplated = "unplated"
"""Unplated icon (transparent background)"""
class IcnsCategory(enum.StrEnum):
Application = "apps"
"""Icons for applications"""
Extension = "exts"