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() Temporary = enum.auto()
Output = enum.auto() Output = enum.auto()
FdOutput = enum.auto() PosixOutput = enum.auto()
PosixFdOutput = enum.auto()
WinOutput = enum.auto() WinOutput = enum.auto()
WinIcoOutput = enum.auto()
WinMsixOutput = enum.auto()
MacOutput = enum.auto() MacOutput = enum.auto()
MacIcnsOutput = enum.auto()
class Address: class Address:
+199 -41
View File
@@ -7,7 +7,17 @@ from pathlib import Path
from typing import TypeVar, Generic, Callable from typing import TypeVar, Generic, Callable
from ..lowlevel.environment import Prologue as LowPrologue, Environment as LowEnv from ..lowlevel.environment import Prologue as LowPrologue, Environment as LowEnv
from ..lowlevel.artrdr import SvgRenderKind 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) @dataclass(frozen=True)
@@ -30,11 +40,7 @@ class Prologue(LowPrologue):
def __post_init__(self) -> None: def __post_init__(self) -> None:
super().__post_init__() super().__post_init__()
for hw in self.fd_thumbnail_hws: for hw in self.fd_thumbnail_hws:
if hw <= 0 or hw > VART_HW: check_fd_hw(hw)
raise ValueError(
f"Given FreeDesktop icon thumbnail size mismatch the size requirements. "
f"Expect >= 0 and <= {VART_HW}, got {hw}."
)
_TEMP_USER_DIR: str = "user" _TEMP_USER_DIR: str = "user"
@@ -42,8 +48,13 @@ _TEMP_LIB_DIR: str = "assemblicon"
_TEMP_LIB_ALLOC_DIR: str = "alloc" _TEMP_LIB_ALLOC_DIR: str = "alloc"
_TEMP_LIB_STABLE_DIR: str = "stable" _TEMP_LIB_STABLE_DIR: str = "stable"
_ART_POSIX: str = "posix"
_ART_POSIX_FD: str = "freedesktop"
_ART_WIN: str = "windows" _ART_WIN: str = "windows"
_ART_WIN_ICO: str = "ico"
_ART_WIN_MSIX: str = "msix"
_ART_MAC: str = "macos" _ART_MAC: str = "macos"
_ART_MAC_ICNS: str = "icns"
P = TypeVar("P", bound=Prologue) P = TypeVar("P", bound=Prologue)
@@ -55,36 +66,11 @@ class Environment(Generic[P]):
"""The lowlevel environment managing temporary directory.""" """The lowlevel environment managing temporary directory."""
def __init__(self, prologue: P) -> None: def __init__(self, prologue: P) -> None:
# assign members
self.__prologue = prologue self.__prologue = prologue
self.__lowenv = LowEnv(self.__prologue) self.__lowenv = LowEnv(self.__prologue)
# build directory hierarchy
# construct basic layout of temporary directory self.__build_dir_hierarchy()
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)
# region: With Visitor Requirements # region: With Visitor Requirements
@@ -102,6 +88,64 @@ class Environment(Generic[P]):
# endregion # 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 # region: Prologue Visitor
@property @property
@@ -176,6 +220,24 @@ class Environment(Generic[P]):
""" """
return self.__lowenv.output_artifact(*args) 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: 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. 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. Required because FreeDesktop need to save icon with different size variant.
:param kind: The FreeDesktop kind of this icon. :param kind: The FreeDesktop kind of this icon.
:param name: The name of saved artifact. The file extension ".png" is not required. :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 kind: The kind of this icon.
:param name: The name of saved artifact. The file extension ".ico" is not required. :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 kind: The kind of this icon.
:param name: The name of saved artifact. The file extension ".icns" is not required. :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 # endregion
+1 -1
View File
@@ -23,7 +23,7 @@ class FdArtSink(Sink):
@property @property
def address(self) -> Address: 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: def push(self, env: Environment, art: Artifact) -> None:
LOGGER.info( LOGGER.info(
@@ -2,28 +2,28 @@ from dataclasses import dataclass
from .common import Sink from .common import Sink
from ..address import Address, AddressDomain from ..address import Address, AddressDomain
from ..environment import Environment from ..environment import Environment
from ..utils import Artifact, MacCategory, check_vart from ..utils import Artifact, IcnsCategory, check_vart
from ...lowlevel import artrdr from ...lowlevel import artrdr
from ...logger import HIGHLEVEL_LOGGER as LOGGER from ...logger import HIGHLEVEL_LOGGER as LOGGER
@dataclass(frozen=True) @dataclass(frozen=True)
class IcnsArtSinkHint: class IcnsArtSinkHint:
kind: MacCategory kind: IcnsCategory
name: str name: str
class IcnsArtSink(Sink): class IcnsArtSink(Sink):
__kind: MacCategory __kind: IcnsCategory
__name: str __name: str
def __init__(self, kind: MacCategory, name: str) -> None: def __init__(self, kind: IcnsCategory, name: str) -> None:
self.__kind = kind self.__kind = kind
self.__name = name self.__name = name
@property @property
def address(self) -> Address: 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: def push(self, env: Environment, art: Artifact) -> None:
LOGGER.info( LOGGER.info(
@@ -32,7 +32,7 @@ class IcnsArtSink(Sink):
self.__name, self.__name,
) )
check_vart(art) 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: def push_hint(self) -> IcnsArtSinkHint:
return IcnsArtSinkHint(self.__kind, self.__name) return IcnsArtSinkHint(self.__kind, self.__name)
@@ -2,28 +2,28 @@ from dataclasses import dataclass
from .common import Sink from .common import Sink
from ..address import Address, AddressDomain from ..address import Address, AddressDomain
from ..environment import Environment from ..environment import Environment
from ..utils import Artifact, WinCategory, check_vart from ..utils import Artifact, IcoCategory, check_vart
from ...lowlevel import artrdr from ...lowlevel import artrdr
from ...logger import HIGHLEVEL_LOGGER as LOGGER from ...logger import HIGHLEVEL_LOGGER as LOGGER
@dataclass(frozen=True) @dataclass(frozen=True)
class IcoArtSinkHint: class IcoArtSinkHint:
kind: WinCategory kind: IcoCategory
name: str name: str
class IcoArtSink(Sink): class IcoArtSink(Sink):
__kind: WinCategory __kind: IcoCategory
__name: str __name: str
def __init__(self, kind: WinCategory, name: str) -> None: def __init__(self, kind: IcoCategory, name: str) -> None:
self.__kind = kind self.__kind = kind
self.__name = name self.__name = name
@property @property
def address(self) -> Address: 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: def push(self, env: Environment, art: Artifact) -> None:
LOGGER.info( LOGGER.info(
@@ -32,7 +32,7 @@ class IcoArtSink(Sink):
self.__name, self.__name,
) )
check_vart(art) 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: def push_hint(self) -> IcoArtSinkHint:
return IcoArtSinkHint(self.__kind, self.__name) return IcoArtSinkHint(self.__kind, self.__name)
@@ -17,7 +17,7 @@ class FdArtSource(Source):
@property @property
def address(self) -> Address: 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: def link_to(self, env: Environment, fd_sink: FdArtSink) -> None:
sink_hint = fd_sink.push_hint() sink_hint = fd_sink.push_hint()
@@ -2,22 +2,22 @@ from .common import Source
from ..address import Address, AddressDomain from ..address import Address, AddressDomain
from ..environment import Environment from ..environment import Environment
from ..sink.icnsart_sink import IcnsArtSink from ..sink.icnsart_sink import IcnsArtSink
from ..utils import MacCategory from ..utils import IcnsCategory
from ...lowlevel import artio from ...lowlevel import artio
from ...logger import HIGHLEVEL_LOGGER as LOGGER from ...logger import HIGHLEVEL_LOGGER as LOGGER
class IcnsArtSource(Source): class IcnsArtSource(Source):
__kind: MacCategory __kind: IcnsCategory
__name: str __name: str
def __init__(self, kind: MacCategory, name: str) -> None: def __init__(self, kind: IcnsCategory, name: str) -> None:
self.__kind = kind self.__kind = kind
self.__name = name self.__name = name
@property @property
def address(self) -> Address: 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: def link_to(self, env: Environment, mac_sink: IcnsArtSink) -> None:
sink_hint = mac_sink.push_hint() sink_hint = mac_sink.push_hint()
@@ -29,6 +29,6 @@ class IcnsArtSource(Source):
sink_hint.name, sink_hint.name,
) )
artio.link_artifact( artio.link_artifact(
env.mac_output_artifact(self.__kind, self.__name), env.icns_output_artifact(self.__kind, self.__name),
env.mac_output_artifact(sink_hint.kind, sink_hint.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 ..address import Address, AddressDomain
from ..environment import Environment from ..environment import Environment
from ..sink.icoart_sink import IcoArtSink from ..sink.icoart_sink import IcoArtSink
from ..utils import WinCategory from ..utils import IcoCategory
from ...lowlevel import artio from ...lowlevel import artio
from ...logger import HIGHLEVEL_LOGGER as LOGGER from ...logger import HIGHLEVEL_LOGGER as LOGGER
class IcoArtSource(Source): class IcoArtSource(Source):
__kind: WinCategory __kind: IcoCategory
__name: str __name: str
def __init__(self, kind: WinCategory, name: str) -> None: def __init__(self, kind: IcoCategory, name: str) -> None:
self.__kind = kind self.__kind = kind
self.__name = name self.__name = name
@property @property
def address(self) -> Address: 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: def link_to(self, env: Environment, win_sink: IcoArtSink) -> None:
sink_hint = win_sink.push_hint() sink_hint = win_sink.push_hint()
@@ -29,6 +29,6 @@ class IcoArtSource(Source):
sink_hint.name, sink_hint.name,
) )
artio.link_artifact( artio.link_artifact(
env.win_output_artifact(self.__kind, self.__name), env.ico_output_artifact(self.__kind, self.__name),
env.win_output_artifact(sink_hint.kind, sink_hint.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): class FdContext(enum.StrEnum):
Actions = "actions" Actions = "actions"
Animations = "animations" Animations = "animations"
@@ -26,14 +46,39 @@ class FdContext(enum.StrEnum):
Status = "status" Status = "status"
class WinCategory(enum.StrEnum): class IcoCategory(enum.StrEnum):
Application = "apps" Application = "apps"
"""Icons for applications""" """Icons for applications"""
Extension = "exts" Extension = "exts"
"""Icons for file extensions""" """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" Application = "apps"
"""Icons for applications""" """Icons for applications"""
Extension = "exts" Extension = "exts"