From c8aaa2268c8d78e91a1dd3b19b2665bf1c80884b Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 5 Dec 2025 14:53:04 +0800 Subject: [PATCH 1/3] Add artifact parsing to Briefcase backend. --- src/projspec/artifact/installable.py | 66 ++++++++++ src/projspec/proj/__init__.py | 2 + src/projspec/proj/briefcase.py | 174 +++++++++++++++++++++++++++ 3 files changed, 242 insertions(+) diff --git a/src/projspec/artifact/installable.py b/src/projspec/artifact/installable.py index 500dbfb..65d2284 100644 --- a/src/projspec/artifact/installable.py +++ b/src/projspec/artifact/installable.py @@ -66,3 +66,69 @@ def clean(self): if self.fn is not None: self.proj.fs.rm(self.fn) self.fn = None + + +class ZipArtifact(FileArtifact): + """A zipfile artifact""" + def __init__(self, proj, fn=None, **kw): + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.zip", **kw) + + +class DMGArtifact(FileArtifact): + """A macOS DMG artifact""" + def __init__(self, proj, fn=None, **kw): + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.dmg", **kw) + + +class PKGArtifact(FileArtifact): + """A macOS PKG artifact""" + def __init__(self, proj, fn=None, **kw): + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.pkg", **kw) + + +class MSIArtifact(FileArtifact): + """A Windows MSI artifact""" + def __init__(self, proj, fn=None, **kw): + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.msi", **kw) + + +class AABArtifact(FileArtifact): + """An Android AAB artifact""" + def __init__(self, proj, fn=None, **kw): + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.aab", **kw) + + +class APKArtifact(FileArtifact): + """An Android APK artifact""" + def __init__(self, proj, fn=None, **kw): + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.apk", **kw) + + +class IPAArtifact(FileArtifact): + """An iOS IPA artifact""" + def __init__(self, proj, fn=None, **kw): + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.ipa", **kw) + + +class RPMArtifact(FileArtifact): + """A Linux RPM artifact""" + def __init__(self, proj, fn=None, **kw): + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.rpm", **kw) + + +class DEBArtifact(FileArtifact): + """A Linux DEB artifact""" + def __init__(self, proj, fn=None, **kw): + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.deb", **kw) + + +class LinuxPKGArtifact(FileArtifact): + """A Linux PKG artifact""" + def __init__(self, proj, fn=None, **kw): + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.pkg.tar.zst", **kw) + + +class FlatpakArtifact(FileArtifact): + """A Linux Flatpak artifact""" + def __init__(self, proj, fn=None, **kw): + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.flatpak", **kw) diff --git a/src/projspec/proj/__init__.py b/src/projspec/proj/__init__.py index 4e635c6..3b9ebad 100644 --- a/src/projspec/proj/__init__.py +++ b/src/projspec/proj/__init__.py @@ -1,4 +1,5 @@ from projspec.proj.base import ParseFailed, Project, ProjectSpec +from projspec.proj.briefcase import Briefcase from projspec.proj.conda_package import CondaRecipe, RattlerRecipe from projspec.proj.conda_project import CondaProject from projspec.proj.documentation import RTD, MDBook @@ -16,6 +17,7 @@ "ParseFailed", "Project", "ProjectSpec", + "Briefcase", "CondaRecipe", "CondaProject", "GitRepo", diff --git a/src/projspec/proj/briefcase.py b/src/projspec/proj/briefcase.py index e635f94..0fa136c 100644 --- a/src/projspec/proj/briefcase.py +++ b/src/projspec/proj/briefcase.py @@ -1,4 +1,37 @@ +import platform +import sys + from projspec.proj import ProjectSpec +from projspec.utils import AttrDict + + +def supported(apps, app, *config): + """Check if an app is supported on a given platform. + + Looks in the metadata for app for a `supported` key in each + level named in config. If there isn't a table for the named config, + or the table contains a `supported = false` declaration, the + + For example, `supported(apps, "foo", "linux", "system")` will check for: + + * A "linux" table; if there isn't, return False + * A `supported` key in the "linux" table (defaulting True) + * A "linux.system" table; if there isn't return False + * A `supported` key in the "linux.system" table (defaulting True) + + If any of those results return False, the app isn't supported. + """ + platform = apps[app].get(config[0], {"supported": False}) + supported = platform.get("supported", True) + for part in config[1:]: + try: + platform = platform.get(part, {"supported": False}) + supported &= platform.get("supported", True) + except KeyError: + # Platform config doesn't exist + supported = False + + return supported class Briefcase(ProjectSpec): @@ -6,3 +39,144 @@ class Briefcase(ProjectSpec): def match(self) -> bool: return "briefcase" in self.proj.pyproject.get("tool", {}) + + def parse(self) -> None: + from projspec.artifact.installable import ( + AABArtifact, + APKArtifact, + DEBArtifact, + DMGArtifact, + IPAArtifact, + LinuxPKGArtifact, + MSIArtifact, + PKGArtifact, + RPMArtifact, + ZipArtifact, + ) + + briefcase_meta = self.proj.pyproject["tool"]["briefcase"] + + cont = AttrDict() + self._contents = cont + + self._artifacts = AttrDict() + + apps = briefcase_meta.get("app", {}) + + if sys.platform == "darwin": + for fmt, Artifact, arg in [ + ("macOS-app", ZipArtifact, "zip"), + ("macOS-dmg", DMGArtifact, "dmg"), + ("macOS-pkg", PKGArtifact, "pkg"), + ]: + for app in apps: + if supported(apps, app, "macOS"): + self._artifacts[fmt] = Artifact( + proj=self.proj, + cmd=["briefcase", "package", "-a", app, "-p", arg], + ) + + # iOS doesn't produce an artifact directly, but it's included for + # completeness. + for app in apps: + if supported(apps, app, "iOS"): + self._artifacts["iOS"] = IPAArtifact( + proj=self.proj, + cmd=["briefcase", "package", "iOS", "-a", app, "-p", "ipa"], + ) + + elif sys.platform == "linux": + # This only covers natively built packages; these can all be built + # via Docker as well. + release = platform.freedesktop_os_release() + release_id = release["ID"] + release_like = release.get("ID_LIKE", "") + + for app in apps: + if release_id == "fedora" or "fedora" in release_like: + if supported(apps, app, "linux", "system", "rhel"): + self._artifacts["linux-rpm"] = Artifact( + proj=self.proj, + cmd=["briefcase", "package", "-a", app, "-p", "rpm"], + ) + elif "suse" in release_like: + if supported(apps, app, "linux", "system", "suse"): + self._artifacts["linux-rpm"] = Artifact( + proj=self.proj, + cmd=["briefcase", "package", "-a", app, "-p", "rpm"], + ) + elif release_id == "debian" or "debian" in release_like: + if supported(apps, app, "linux", "system", "debian"): + self._artifacts["linux-deb"] = Artifact( + proj=self.proj, + cmd=["briefcase", "package", "-a", app, "-p", "deb"], + ) + elif release_id == "arch" or "arch" in release_like: + if supported(apps, app, "linux", "system", "arch"): + self._artifacts["linux-pkg"] = Artifact( + proj=self.proj, + cmd=["briefcase", "package", "-a", app, "-p", "pkg"], + ) + + if supported(apps, app, "linux", "flatpak"): + self._artifacts["linux-flatpak"] = IPAArtifact( + proj=self.proj, + cmd=[ + "briefcase", + "package", + "linux", + "flatpak", + "-a", + app, + "-p", + "flatpak", + ], + ) + + elif sys.platform == "windows": + for fmt, Artifact, arg in [ + ("windows-app", ZipArtifact, "zip"), + ("windows-msi", MSIArtifact, "msi"), + ]: + for app in apps: + if supported(apps, app, "windows"): + self._artifacts[fmt] = Artifact( + proj=self.proj, + cmd=["briefcase", "package", "-a", app, "-p", arg], + ) + + # Android apps can be built on every platform + for app in apps: + if supported(apps, app, "android"): + for fmt, Artifact, arg in [ + ("android-aab", AABArtifact, "aab"), + ("android-apk", APKArtifact, "apk"), + ]: + self._artifacts[fmt] = Artifact( + proj=self.proj, + cmd=[ + "briefcase", + "package", + "android", + "-a", + app, + "-p", + arg, + ], + ) + + # Web apps can be built on every platform + for app in apps: + if supported(apps, app, "web"): + self._artifacts["web-zip"] = Artifact( + proj=self.proj, + cmd=[ + "briefcase", + "package", + "web", + "-a", + app, + "-p", + "zip", + ], + ) From c43e00af5289cb3d9ba4f99a5a70898efe58505f Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Fri, 5 Dec 2025 15:43:15 +0800 Subject: [PATCH 2/3] Differentiate Web and MacOS zip artefacts. --- src/projspec/artifact/installable.py | 12 +++++++++--- src/projspec/proj/briefcase.py | 23 ++++++++++------------- 2 files changed, 19 insertions(+), 16 deletions(-) diff --git a/src/projspec/artifact/installable.py b/src/projspec/artifact/installable.py index 65d2284..f4fb560 100644 --- a/src/projspec/artifact/installable.py +++ b/src/projspec/artifact/installable.py @@ -68,10 +68,10 @@ def clean(self): self.fn = None -class ZipArtifact(FileArtifact): - """A zipfile artifact""" +class MacOSZipArtifact(FileArtifact): + """A zipped macOS app artifact""" def __init__(self, proj, fn=None, **kw): - super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.zip", **kw) + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.app.zip", **kw) class DMGArtifact(FileArtifact): @@ -132,3 +132,9 @@ class FlatpakArtifact(FileArtifact): """A Linux Flatpak artifact""" def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.flatpak", **kw) + + +class WebZipArtifact(FileArtifact): + """A static website zipfile artifact""" + def __init__(self, proj, fn=None, **kw): + super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.web.zip", **kw) diff --git a/src/projspec/proj/briefcase.py b/src/projspec/proj/briefcase.py index 0fa136c..27e5130 100644 --- a/src/projspec/proj/briefcase.py +++ b/src/projspec/proj/briefcase.py @@ -48,10 +48,11 @@ def parse(self) -> None: DMGArtifact, IPAArtifact, LinuxPKGArtifact, + MacOSZipArtifact, MSIArtifact, PKGArtifact, RPMArtifact, - ZipArtifact, + WebZipArtifact, ) briefcase_meta = self.proj.pyproject["tool"]["briefcase"] @@ -65,7 +66,7 @@ def parse(self) -> None: if sys.platform == "darwin": for fmt, Artifact, arg in [ - ("macOS-app", ZipArtifact, "zip"), + ("macOS-app", MacOSZipArtifact, "zip"), ("macOS-dmg", DMGArtifact, "dmg"), ("macOS-pkg", PKGArtifact, "pkg"), ]: @@ -134,16 +135,12 @@ def parse(self) -> None: ) elif sys.platform == "windows": - for fmt, Artifact, arg in [ - ("windows-app", ZipArtifact, "zip"), - ("windows-msi", MSIArtifact, "msi"), - ]: - for app in apps: - if supported(apps, app, "windows"): - self._artifacts[fmt] = Artifact( - proj=self.proj, - cmd=["briefcase", "package", "-a", app, "-p", arg], - ) + for app in apps: + if supported(apps, app, "windows"): + self._artifacts["windows-msi"] = MSIArtifact( + proj=self.proj, + cmd=["briefcase", "package", "-a", app, "-p", "msi"], + ) # Android apps can be built on every platform for app in apps: @@ -168,7 +165,7 @@ def parse(self) -> None: # Web apps can be built on every platform for app in apps: if supported(apps, app, "web"): - self._artifacts["web-zip"] = Artifact( + self._artifacts["web-zip"] = WebZipArtifact( proj=self.proj, cmd=[ "briefcase", From b18d8d4c7b37c1e9793fe4e37db779c71e0f5a49 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 10 Dec 2025 07:58:16 +0800 Subject: [PATCH 3/3] Correct linting errors. --- src/projspec/artifact/installable.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/projspec/artifact/installable.py b/src/projspec/artifact/installable.py index f4fb560..1c92b83 100644 --- a/src/projspec/artifact/installable.py +++ b/src/projspec/artifact/installable.py @@ -70,71 +70,83 @@ def clean(self): class MacOSZipArtifact(FileArtifact): """A zipped macOS app artifact""" + def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.app.zip", **kw) class DMGArtifact(FileArtifact): """A macOS DMG artifact""" + def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.dmg", **kw) class PKGArtifact(FileArtifact): """A macOS PKG artifact""" + def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.pkg", **kw) class MSIArtifact(FileArtifact): """A Windows MSI artifact""" + def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.msi", **kw) class AABArtifact(FileArtifact): """An Android AAB artifact""" + def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.aab", **kw) class APKArtifact(FileArtifact): """An Android APK artifact""" + def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.apk", **kw) class IPAArtifact(FileArtifact): """An iOS IPA artifact""" + def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.ipa", **kw) class RPMArtifact(FileArtifact): """A Linux RPM artifact""" + def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.rpm", **kw) class DEBArtifact(FileArtifact): """A Linux DEB artifact""" + def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.deb", **kw) class LinuxPKGArtifact(FileArtifact): """A Linux PKG artifact""" + def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.pkg.tar.zst", **kw) class FlatpakArtifact(FileArtifact): """A Linux Flatpak artifact""" + def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.flatpak", **kw) class WebZipArtifact(FileArtifact): """A static website zipfile artifact""" + def __init__(self, proj, fn=None, **kw): super().__init__(proj=proj, fn=fn or f"{proj.url}/dist/*.web.zip", **kw)