-
-
Notifications
You must be signed in to change notification settings - Fork 415
build: refactor Flatpak packaging to use online builds and force X11 … #339
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
c2bfddd
466c697
3c8aca8
7c807b3
43da234
8e0aa65
ae326d1
10f49f8
81d4bbe
94d1f66
b17d5b6
975e91b
2c84342
e49e4a9
000495a
d2e82a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| "permissions": { | ||
| "allow": [ | ||
| "Bash(grep -r \"\\\\.FLATPAK\\\\|Flatpak\\\\|flatpak\" D:/development/Github-Store --include=*.kt)" | ||
| ] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| # Flatpak packaging files must use Unix LF line endings | ||
| # (flatpak-builder on Linux can't parse JSON with CRLF) | ||
| packaging/flatpak/*.json text eol=lf | ||
| packaging/flatpak/*.yml text eol=lf | ||
| packaging/flatpak/*.sh text eol=lf |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,19 @@ class DesktopInstaller( | |
| determineSystemArchitecture() | ||
| } | ||
|
|
||
| /** | ||
| * Detects whether the app is running inside a Flatpak sandbox. | ||
| * Checks for the `/.flatpak-info` file which is always present inside Flatpak containers. | ||
| */ | ||
| private val isRunningInFlatpak: Boolean by lazy { | ||
| try { | ||
| File("/.flatpak-info").exists() || | ||
| System.getenv("FLATPAK_ID") != null | ||
| } catch (_: Exception) { | ||
| false | ||
| } | ||
| } | ||
|
|
||
| override fun getApkInfoExtractor(): InstallerInfoExtractor = installerInfoExtractor | ||
|
|
||
| override fun detectSystemArchitecture(): SystemArchitecture = systemArchitecture | ||
|
|
@@ -54,13 +67,11 @@ class DesktopInstaller( | |
| } | ||
|
|
||
| override fun openApp(packageName: String): Boolean { | ||
| // Desktop apps are launched differently per platform | ||
| Logger.d { "Open app not supported on desktop for: $packageName" } | ||
| return false | ||
| } | ||
|
|
||
| override fun openWithExternalInstaller(filePath: String) { | ||
| // Not applicable on desktop | ||
| } | ||
|
|
||
| override fun isAssetInstallable(assetName: String): Boolean { | ||
|
|
@@ -108,10 +119,18 @@ class DesktopInstaller( | |
| } | ||
|
|
||
| Platform.LINUX -> { | ||
| when (linuxPackageType) { | ||
| LinuxPackageType.DEB -> listOf(".appimage", ".deb", ".rpm") | ||
| LinuxPackageType.RPM -> listOf(".appimage", ".rpm", ".deb") | ||
| LinuxPackageType.UNIVERSAL -> listOf(".appimage", ".deb", ".rpm") | ||
| if (isRunningInFlatpak) { | ||
| when (linuxPackageType) { | ||
| LinuxPackageType.DEB -> listOf(".deb", ".appimage", ".rpm") | ||
| LinuxPackageType.RPM -> listOf(".rpm", ".appimage", ".deb") | ||
| LinuxPackageType.UNIVERSAL -> listOf(".appimage", ".deb", ".rpm") | ||
| } | ||
| } else { | ||
| when (linuxPackageType) { | ||
| LinuxPackageType.DEB -> listOf(".appimage", ".deb", ".rpm") | ||
| LinuxPackageType.RPM -> listOf(".appimage", ".rpm", ".deb") | ||
| LinuxPackageType.UNIVERSAL -> listOf(".appimage", ".deb", ".rpm") | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -174,6 +193,15 @@ class DesktopInstaller( | |
| private fun determineLinuxPackageType(): LinuxPackageType { | ||
| if (platform != Platform.LINUX) return LinuxPackageType.UNIVERSAL | ||
|
|
||
| if (isRunningInFlatpak) { | ||
| return try { | ||
| detectHostLinuxPackageType() | ||
| } catch (e: Exception) { | ||
| Logger.w { "Failed to detect host Linux package type from Flatpak: ${e.message}" } | ||
| LinuxPackageType.UNIVERSAL | ||
| } | ||
| } | ||
|
|
||
| return try { | ||
| val osRelease = tryReadOsRelease() | ||
| if (osRelease != null) { | ||
|
|
@@ -233,6 +261,43 @@ class DesktopInstaller( | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * When running inside a Flatpak sandbox, /etc/os-release belongs to the Flatpak runtime | ||
| * (e.g. org.freedesktop.Platform), not the host OS. To detect the host distro we read | ||
| * /run/host/os-release, which Flatpak bind-mounts from the host. | ||
| */ | ||
| private fun detectHostLinuxPackageType(): LinuxPackageType { | ||
| val hostOsRelease = File("/run/host/os-release") | ||
| if (!hostOsRelease.exists()) { | ||
| Logger.w { "Host os-release not available at /run/host/os-release" } | ||
| return LinuxPackageType.UNIVERSAL | ||
| } | ||
|
|
||
| val osRelease = parseOsRelease(hostOsRelease.readText()) | ||
| val id = osRelease["ID"]?.lowercase() ?: "" | ||
| val idLike = osRelease["ID_LIKE"]?.lowercase() ?: "" | ||
|
|
||
| Logger.d { "Host distro detected from Flatpak: ID=$id, ID_LIKE=$idLike" } | ||
|
|
||
| if (id in listOf("debian", "ubuntu", "linuxmint", "pop", "elementary") || | ||
| idLike.contains("debian") || idLike.contains("ubuntu") | ||
| ) { | ||
| Logger.d { "Host is Debian-based: $id" } | ||
| return LinuxPackageType.DEB | ||
| } | ||
|
|
||
| if (id in listOf("fedora", "rhel", "centos", "rocky", "almalinux", "opensuse", "suse") || | ||
| idLike.contains("fedora") || idLike.contains("rhel") || | ||
| idLike.contains("suse") || idLike.contains("centos") | ||
| ) { | ||
| Logger.d { "Host is RPM-based: $id" } | ||
| return LinuxPackageType.RPM | ||
| } | ||
|
|
||
| Logger.d { "Could not classify host distro, defaulting to UNIVERSAL" } | ||
| return LinuxPackageType.UNIVERSAL | ||
| } | ||
|
|
||
| private fun tryReadOsRelease(): Map<String, String>? { | ||
| val osReleaseFiles = | ||
| listOf( | ||
|
|
@@ -318,6 +383,11 @@ class DesktopInstaller( | |
| withContext(Dispatchers.IO) { | ||
| val ext = extOrMime.lowercase().removePrefix(".") | ||
|
|
||
| if (isRunningInFlatpak) { | ||
| Logger.d { "Running in Flatpak — skipping permission checks for .$ext" } | ||
| return@withContext | ||
| } | ||
|
|
||
| if (platform == Platform.LINUX && ext == "appimage") { | ||
| try { | ||
| val tempFile = File.createTempFile("appimage_perm_test", ".tmp") | ||
|
|
@@ -326,7 +396,7 @@ class DesktopInstaller( | |
| if (!canSetExecutable) { | ||
| throw IllegalStateException( | ||
| "Unable to set executable permissions. AppImage installation requires " + | ||
| "the ability to make files executable.", | ||
| "the ability to make files executable.", | ||
| ) | ||
| } | ||
| } finally { | ||
|
|
@@ -347,7 +417,6 @@ class DesktopInstaller( | |
| } | ||
|
|
||
| override fun uninstall(packageName: String) { | ||
| // Desktop doesn't have a unified uninstall mechanism | ||
| Logger.d { "Uninstall not supported on desktop for: $packageName" } | ||
| } | ||
|
|
||
|
|
@@ -363,6 +432,11 @@ class DesktopInstaller( | |
|
|
||
| val ext = extOrMime.lowercase().removePrefix(".") | ||
|
|
||
| if (isRunningInFlatpak) { | ||
| installFromFlatpak(file, ext) | ||
| return@withContext InstallOutcome.DELEGATED_TO_SYSTEM | ||
| } | ||
|
|
||
| when (platform) { | ||
| Platform.WINDOWS -> installWindows(file, ext) | ||
| Platform.MACOS -> installMacOS(file, ext) | ||
|
|
@@ -371,7 +445,145 @@ class DesktopInstaller( | |
| } | ||
|
|
||
| InstallOutcome.DELEGATED_TO_SYSTEM | ||
|
|
||
| } | ||
|
|
||
| /** | ||
| * Flatpak-sandboxed installation flow. | ||
| * | ||
| * Since we can't execute system installers, we use xdg-open (which goes through | ||
| * the Flatpak portal to the host) to open the file with the host's default handler. | ||
| * This lets the host's software center / file manager handle the actual installation. | ||
| */ | ||
| private fun installFromFlatpak( | ||
| file: File, | ||
| ext: String, | ||
| ) { | ||
| Logger.i { "Running in Flatpak sandbox — delegating installation to host system" } | ||
| Logger.i { "File: ${file.absolutePath} (.$ext)" } | ||
|
|
||
| when (ext) { | ||
| "deb", "rpm" -> { | ||
| Logger.d { "Opening .$ext package via xdg-open portal for host installation" } | ||
| try { | ||
| val process = ProcessBuilder("xdg-open", file.absolutePath).start() | ||
| val exitCode = process.waitFor() | ||
| if (exitCode == 0) { | ||
| Logger.i { "Package opened on host system for installation" } | ||
| showFlatpakNotification( | ||
| title = "Package Ready to Install", | ||
| message = "The ${ext.uppercase()} package has been opened in your system's " + | ||
| "software installer. Follow the prompts to complete installation.", | ||
| ) | ||
| } else { | ||
| Logger.w { "xdg-open exited with code $exitCode" } | ||
| showFlatpakNotification( | ||
| title = "Installation", | ||
| message = "Please open this file with your software center to install.", | ||
| ) | ||
| openInFileManager(file) | ||
| } | ||
| } catch (e: Exception) { | ||
| Logger.w { "Failed to open file via xdg-open: ${e.message}" } | ||
| showFlatpakNotification( | ||
| title = "Download Complete", | ||
| message = "Please install manually from your file manager.", | ||
| ) | ||
| openInFileManager(file) | ||
| } | ||
| } | ||
|
|
||
| "appimage" -> { | ||
| Logger.d { "AppImage downloaded in Flatpak — preparing for host launch" } | ||
|
|
||
| try { | ||
| file.setExecutable(true, false) | ||
| Logger.d { "Set executable permission on AppImage" } | ||
| } catch (e: Exception) { | ||
| Logger.w { "Could not set executable permission: ${e.message}" } | ||
| } | ||
|
|
||
| showFlatpakNotification( | ||
| title = "AppImage Downloaded", | ||
| message = "Right-click → Properties → mark as executable, then double-click to run.", | ||
| ) | ||
|
|
||
| openInFileManager(file) | ||
| } | ||
|
|
||
| else -> { | ||
| showFlatpakNotification( | ||
| title = "Download Complete", | ||
| message = "File saved to your Downloads folder.", | ||
| ) | ||
| openInFileManager(file) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Show a notification from within the Flatpak sandbox. | ||
| * Uses notify-send which goes through the desktop notifications portal. | ||
| * Falls back to logging if notifications aren't available. | ||
| */ | ||
| private fun showFlatpakNotification( | ||
| title: String, | ||
| message: String, | ||
| ) { | ||
| try { | ||
| ProcessBuilder( | ||
| "notify-send", | ||
| "--app-name=GitHub Store", | ||
| title, | ||
| message, | ||
| "-u", | ||
| "normal", | ||
| "-t", | ||
| "15000", | ||
| ).start() | ||
| } catch (e: Exception) { | ||
| Logger.w { "Could not show Flatpak notification: ${e.message}" } | ||
| Logger.i { "[$title] $message" } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Opens the system file manager with the given file highlighted/selected. | ||
| * | ||
| * Tries D-Bus FileManager1.ShowItems first (works on GNOME, KDE, etc. and | ||
| * goes through the Flatpak portal), then falls back to xdg-open on the | ||
| * parent directory. | ||
| */ | ||
| private fun openInFileManager(file: File) { | ||
| try { | ||
| val fileUri = "file://${file.absolutePath}" | ||
| val process = ProcessBuilder( | ||
| "gdbus", "call", | ||
| "--session", | ||
| "--dest", "org.freedesktop.FileManager1", | ||
| "--object-path", "/org/freedesktop/FileManager1", | ||
| "--method", "org.freedesktop.FileManager1.ShowItems", | ||
| "['$fileUri']", "", | ||
| ).start() | ||
| val exitCode = process.waitFor() | ||
|
|
||
| if (exitCode == 0) { | ||
| Logger.d { "Opened file manager via D-Bus ShowItems: ${file.absolutePath}" } | ||
| return | ||
| } | ||
| Logger.w { "D-Bus ShowItems failed with exit code $exitCode" } | ||
| } catch (e: Exception) { | ||
| Logger.w { "D-Bus ShowItems not available: ${e.message}" } | ||
| } | ||
|
Comment on lines
+557
to
+577
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check if FileManager1 D-Bus permission is declared
grep -r "FileManager1" packaging/flatpak/ 2>/dev/null || echo "No FileManager1 permission found"
echo "---"
# Check finish-args for talk-name permissions
grep -A 30 "finish-args" packaging/flatpak/zed.rainxch.githubstore.yml | grep "talk-name" || echo "No talk-name permissions declared"Repository: OpenHub-Store/GitHub-Store Length of output: 140 🏁 Script executed: #!/bin/bash
# Check if file exists and get line count
if [ -f "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt" ]; then
wc -l "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt"
else
echo "File not found at expected path"
# Try to find it
find . -name "DesktopInstaller.kt" 2>/dev/null
fiRepository: OpenHub-Store/GitHub-Store Length of output: 154 🏁 Script executed: #!/bin/bash
# Read the openInFileManager method context (lines 575-614)
if [ -f "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt" ]; then
echo "=== Lines 575-614 ==="
sed -n '575,614p' "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt"
else
echo "File not found, searching..."
find . -name "DesktopInstaller.kt" -exec echo "Found: {}" \; -exec head -650 {} \; 2>/dev/null | tail -100
fiRepository: OpenHub-Store/GitHub-Store Length of output: 1794 🏁 Script executed: #!/bin/bash
# Verify package naming compliance with coding guideline: zed.rainxch.{module}.{layer}
grep -n "^package " "core/data/src/jvmMain/kotlin/zed/rainxch/core/data/services/DesktopInstaller.kt" 2>/dev/null || echo "Package declaration not found"Repository: OpenHub-Store/GitHub-Store Length of output: 110 D-Bus FileManager1 call will fail in Flatpak; GVariant argument is injection-vulnerable. Two issues:
🤖 Prompt for AI Agents |
||
|
|
||
| try { | ||
| val parentDir = file.parentFile ?: return | ||
| ProcessBuilder("xdg-open", parentDir.absolutePath).start() | ||
| Logger.d { "Opened parent directory: ${parentDir.absolutePath}" } | ||
| } catch (e: Exception) { | ||
| Logger.w { "Could not open file manager: ${e.message}" } | ||
| } | ||
| } | ||
|
|
||
| private fun installWindows( | ||
| file: File, | ||
|
|
@@ -507,7 +719,12 @@ class DesktopInstaller( | |
| val installMethods = | ||
| listOf( | ||
| listOf("pkexec", "apt", "install", "-y", file.absolutePath), | ||
| listOf("pkexec", "sh", "-c", "dpkg -i '${file.absolutePath}' || apt-get install -f -y"), | ||
| listOf( | ||
| "pkexec", | ||
| "sh", | ||
| "-c", | ||
| "dpkg -i '${file.absolutePath}' || apt-get install -f -y" | ||
| ), | ||
| listOf("gdebi-gtk", file.absolutePath), | ||
| null, | ||
| ) | ||
|
|
@@ -899,7 +1116,7 @@ class DesktopInstaller( | |
| e.printStackTrace() | ||
| throw IllegalStateException( | ||
| "Failed to install AppImage: ${e.message}. " + | ||
| "Please ensure you have write permissions to ~/Applications folder.", | ||
| "Please ensure you have write permissions to ~/Applications folder.", | ||
| e, | ||
| ) | ||
| } catch (e: SecurityException) { | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.