Something went wrong. Try again.
Manifests and a static winget source for jcollie's Windows programs, built and signed on Linux.
Something went wrong. Try again.
10.0 kB · 245 lines
Python
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246# SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie <jeff@ocjtech.us># SPDX-License-Identifier: MIT
"""Packing the index into an (unsigned) MSIX.
An MSIX is a ZIP file with three extra parts: `AppxManifest.xml`, which sayswhat the package is; `AppxBlockMap.xml`, the SHA-256 of every 64 KiB block ofevery other file; and `[Content_Types].xml`, the OPC media types. Signing(osslsigncode) then hashes those and adds `AppxSignature.p7x`. Nothing elseis needed, so this builds the container directly rather than depending on`makemsix`, which nixpkgs does not carry.
Every file in the block map is stored uncompressed. A compressed file's block map has to givethe compressed size of each block, which only works if each 64 KiB block isdeflated on its own; storing avoids that entirely, the block map then carriesno sizes, and the published package is fetched once per update anyway.
The manifest follows the one in Microsoft's own `source.msix`: winget findsthe index through the `com.microsoft.winget.source` app extension with the Id`IndexDB`, reads `Public\\index.db` from its public folder, and, when it runsas the App Installer package, deploys the whole thing with the packagemanager, which is why it has to be a well-formed package with logos."""
import base64import datetimeimport hashlibimport ioimport structimport zlibfrom xml.sax.saxutils import escape, quoteattr
BLOCK_SIZE = 65536
_CONTENT_TYPES = { "db": "application/octet-stream", "png": "image/png", "xml": "application/vnd.ms-appx.manifest+xml",}
_APPX_MANIFEST = """\<?xml version="1.0" encoding="utf-8"?><Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10" xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10" xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3" IgnorableNamespaces="uap uap3">
<Identity Name={name} ProcessorArchitecture="neutral" Publisher={publisher} Version={version} />
<Properties> <DisplayName>{display_name}</DisplayName> <PublisherDisplayName>{publisher_display_name}</PublisherDisplayName> <Logo>Assets\\StoreLogo.png</Logo> </Properties>
<Dependencies> <TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.16299.0" MaxVersionTested="10.0.26100.0" /> </Dependencies>
<Applications> <Application Id="SourceData"> <uap:VisualElements DisplayName={display_name_attr} Square150x150Logo="Assets\\Square150x150Logo.png" Square44x44Logo="Assets\\Square44x44Logo.png" Description={display_name_attr} BackgroundColor="transparent" AppListEntry="none" /> <Extensions> <uap3:Extension Category="windows.appExtension"> <uap3:AppExtension Name="com.microsoft.winget.source" Id="IndexDB" DisplayName={display_name_attr} Description={display_name_attr} PublicFolder="Public"> </uap3:AppExtension> </uap3:Extension> </Extensions> </Application> </Applications>
<Resources> <Resource Language="und" /> </Resources>
</Package>"""
def version_from_time(when: datetime.datetime) -> str: """The package version for a point in time, in the form winget's `GetTimePointFromVersion` reads back: year, month and day, hour and minute, then milliseconds (here, seconds times 1000).""" when = when.astimezone(datetime.timezone.utc) return f"{when.year}.{when.month * 100 + when.day}.{when.hour * 100 + when.minute}.{when.second * 1000}"
def _png(size: int, rgb: tuple[int, int, int]) -> bytes: """A square PNG of one color."""
def chunk(kind: bytes, data: bytes) -> bytes: return struct.pack(">I", len(data)) + kind + data + struct.pack(">I", zlib.crc32(kind + data))
row = b"\x00" + bytes(rgb) * size return ( b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", size, size, 8, 2, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(row * size, 9)) + chunk(b"IEND", b"") )
def _block_map(entries: list[tuple[str, bytes, int]]) -> bytes: files = [] for name, data, lfh_size in entries: blocks = "".join( f'<Block Hash="{base64.b64encode(hashlib.sha256(data[i : i + BLOCK_SIZE]).digest()).decode()}"/>' for i in range(0, len(data), BLOCK_SIZE) ) files.append(f'<File Name={quoteattr(name.replace("/", chr(92)))} Size="{len(data)}" LfhSize="{lfh_size}">{blocks}</File>') return ( '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n' '<BlockMap xmlns="http://schemas.microsoft.com/appx/2010/blockmap"' ' HashMethod="http://www.w3.org/2001/04/xmlenc#sha256">' + "".join(files) + "</BlockMap>" ).encode()
def _content_types(names: list[str]) -> bytes: defaults = sorted({n.rsplit(".", 1)[-1].lower() for n in names}) return ( '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' '<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">' + "".join(f'<Default Extension="{ext}" ContentType="{_CONTENT_TYPES[ext]}"/>' for ext in defaults) + '<Override PartName="/AppxBlockMap.xml" ContentType="application/vnd.ms-appx.blockmap+xml"/>' + "</Types>" ).encode()
class _ZipWriter: """The ZIP layout Microsoft's packaging tools write, which is the only one Windows will open: it refuses a plain ZIP as a bad format (0x8007000B) even where Microsoft's open-source reader accepts it.
That layout is ZIP64 throughout. Each payload file is written with the data-descriptor flag, a local header with no sizes, and a ZIP64 data descriptor after the data; the footprint files that signing rewrites are plain entries; and the end of the central directory is a ZIP64 record and locator, with the classic record left holding only placeholders. """
# Version 4.5, the first with ZIP64; the high byte, 0, is MS-DOS. _ZIP64 = 45 _DEFAULT = 20 _DATA_DESCRIPTOR = 0x0008 # 1980-01-01 00:00, the DOS epoch, so that the package is reproducible. _TIME, _DATE = 0, (0 << 9) | (1 << 5) | 1
def __init__(self): self.out = io.BytesIO() self.central = []
def add(self, name: str, data: bytes, *, deflate: bool = False, descriptor: bool = True) -> int: """Write one entry and return the length of its local header.""" encoded = name.encode() crc = zlib.crc32(data) if deflate: compressor = zlib.compressobj(9, zlib.DEFLATED, -15) stored = compressor.compress(data) + compressor.flush() else: stored = data method = 8 if deflate else 0 version = self._ZIP64 if descriptor else self._DEFAULT flags = self._DATA_DESCRIPTOR if descriptor else 0 offset = self.out.tell()
header = struct.pack( "<4sHHHHHIIIHH", b"PK\x03\x04", version, flags, method, self._TIME, self._DATE, *((0, 0, 0) if descriptor else (crc, len(stored), len(data))), len(encoded), 0, ) self.out.write(header + encoded + stored) if descriptor: self.out.write(struct.pack("<4sIQQ", b"PK\x07\x08", crc, len(stored), len(data)))
self.central.append( struct.pack( "<4sHHHHHHIIIHHHHHII", b"PK\x01\x02", self._ZIP64, version, flags, method, self._TIME, self._DATE, crc, len(stored), len(data), len(encoded), 0, 0, 0, 0, 0, offset, ) + encoded ) return len(header) + len(encoded)
def finish(self) -> bytes: start = self.out.tell() directory = b"".join(self.central) self.out.write(directory) record = self.out.tell() count = len(self.central) self.out.write( struct.pack( "<4sQHHIIQQQQ", b"PK\x06\x06", 44, self._ZIP64, self._ZIP64, 0, 0, count, count, len(directory), start, ) ) self.out.write(struct.pack("<4sIQI", b"PK\x06\x07", 0, record, 1)) self.out.write(struct.pack("<4sHHHHIIH", b"PK\x05\x06", 0, 0, 0xFFFF, 0xFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0)) return self.out.getvalue()
def build(index_db: bytes, *, name: str, publisher: str, version: str, display_name: str, publisher_display_name: str) -> bytes: """An unsigned MSIX holding `index_db` at `Public\\index.db`.""" manifest = _APPX_MANIFEST.format( name=quoteattr(name), publisher=quoteattr(publisher), version=quoteattr(version), display_name=escape(display_name), display_name_attr=quoteattr(display_name), publisher_display_name=escape(publisher_display_name), ).encode()
payload = [ ("Assets/StoreLogo.png", _png(50, (0x2B, 0x57, 0x9A))), ("Assets/Square44x44Logo.png", _png(44, (0x2B, 0x57, 0x9A))), ("Assets/Square150x150Logo.png", _png(150, (0x2B, 0x57, 0x9A))), ("Public/index.db", index_db), ("AppxManifest.xml", manifest), ]
z = _ZipWriter() mapped = [] for path, data in payload: mapped.append((path, data, z.add(path, data))) z.add("AppxBlockMap.xml", _block_map(mapped)) # The content types are outside the block map, so they can be compressed, # and must be: osslsigncode 2.14 rewrites this file when it signs, # deflating what it writes but keeping the original method, so a stored # original comes out marked stored with deflated contents. It is also a # plain entry, as in Microsoft's packages, since signing rewrites it. z.add("[Content_Types].xml", _content_types([p for p, _ in payload]), deflate=True, descriptor=False) return z.finish()