Skip to content

Commit 6a3f733

Browse files
committed
feat: add Flatpak packaging support
- Introduce Flatpak build configuration including manifest (`.yml`), AppStream metainfo, and desktop entry. - Add a specialized shell script `disable-android-for-flatpak.sh` to strip Android-specific plugins and configurations in-place, enabling builds in SDK-restricted environments. - Implement a launcher script to execute the application using a bundled JetBrains Runtime (JBR) for optimal Compose Desktop performance. - Patch Gradle convention plugins to provide no-op implementations for Android targets during the Flatpak build process. - Configure Flatpak sandbox permissions for network access, X11/Wayland display, and XDG download directory integration.
1 parent 42f8b5a commit 6a3f733

5 files changed

Lines changed: 435 additions & 0 deletions

File tree

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
#!/bin/bash
2+
# disable-android-for-flatpak.sh
3+
#
4+
# Strips all Android-related configuration from the project so it can
5+
# build inside the Flatpak sandbox where no Android SDK is available.
6+
# This script modifies files IN-PLACE — only run during Flatpak builds.
7+
8+
set -euo pipefail
9+
10+
echo "=== Disabling Android targets for Flatpak build ==="
11+
12+
# ─────────────────────────────────────────────────────────────────────
13+
# 1. Root build.gradle.kts — comment out Android plugin declarations
14+
# ─────────────────────────────────────────────────────────────────────
15+
echo "[1/6] Patching root build.gradle.kts"
16+
sed -i \
17+
-e 's|alias(libs.plugins.android.application)|// alias(libs.plugins.android.application)|' \
18+
-e 's|alias(libs.plugins.android.library)|// alias(libs.plugins.android.library)|' \
19+
-e 's|alias(libs.plugins.android.kotlin.multiplatform.library)|// alias(libs.plugins.android.kotlin.multiplatform.library)|' \
20+
build.gradle.kts
21+
22+
# ─────────────────────────────────────────────────────────────────────
23+
# 2. Convention plugins — replace Android plugin applies with no-ops
24+
# ─────────────────────────────────────────────────────────────────────
25+
CONVENTION_DIR="build-logic/convention/src/main/kotlin"
26+
27+
echo "[2/6] Patching KmpLibraryConventionPlugin (remove Android library plugin + config)"
28+
cat > "$CONVENTION_DIR/KmpLibraryConventionPlugin.kt" << 'KOTLIN'
29+
import org.gradle.api.Plugin
30+
import org.gradle.api.Project
31+
import org.gradle.kotlin.dsl.dependencies
32+
import zed.rainxch.githubstore.convention.configureJvmTarget
33+
import zed.rainxch.githubstore.convention.libs
34+
import zed.rainxch.githubstore.convention.pathToResourcePrefix
35+
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
36+
import org.gradle.kotlin.dsl.configure
37+
38+
class KmpLibraryConventionPlugin : Plugin<Project> {
39+
override fun apply(target: Project) {
40+
with(target) {
41+
with(pluginManager) {
42+
apply("org.jetbrains.kotlin.multiplatform")
43+
apply("org.jetbrains.kotlin.plugin.serialization")
44+
}
45+
46+
configureJvmTarget()
47+
48+
extensions.configure<KotlinMultiplatformExtension> {
49+
compilerOptions {
50+
freeCompilerArgs.add("-Xexpect-actual-classes")
51+
freeCompilerArgs.add("-Xmulti-dollar-interpolation")
52+
freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn")
53+
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
54+
}
55+
}
56+
57+
dependencies {
58+
"commonMainImplementation"(libs.findLibrary("kotlinx-serialization-json").get())
59+
}
60+
}
61+
}
62+
}
63+
KOTLIN
64+
65+
echo "[3/6] Patching CmpApplicationConventionPlugin (remove Android application)"
66+
cat > "$CONVENTION_DIR/CmpApplicationConventionPlugin.kt" << 'KOTLIN'
67+
import org.gradle.api.Plugin
68+
import org.gradle.api.Project
69+
import org.gradle.kotlin.dsl.dependencies
70+
import zed.rainxch.githubstore.convention.configureJvmTarget
71+
import zed.rainxch.githubstore.convention.libs
72+
73+
class CmpApplicationConventionPlugin : Plugin<Project> {
74+
override fun apply(target: Project) {
75+
with(target) {
76+
with(pluginManager) {
77+
apply("org.jetbrains.kotlin.multiplatform")
78+
apply("org.jetbrains.compose")
79+
apply("org.jetbrains.kotlin.plugin.compose")
80+
}
81+
82+
configureJvmTarget()
83+
}
84+
}
85+
}
86+
KOTLIN
87+
88+
echo "[4/6] Patching CmpLibraryConventionPlugin & CmpFeatureConventionPlugin"
89+
90+
# CmpLibraryConventionPlugin — remove Android library dependency
91+
sed -i \
92+
-e 's|apply("com.android.library")|// apply("com.android.library")|' \
93+
"$CONVENTION_DIR/CmpLibraryConventionPlugin.kt" 2>/dev/null || true
94+
95+
sed -i \
96+
-e 's|apply("com.android.library")|// apply("com.android.library")|' \
97+
"$CONVENTION_DIR/CmpFeatureConventionPlugin.kt" 2>/dev/null || true
98+
99+
# Remove configureKotlinAndroid calls and Android extension blocks
100+
for f in "$CONVENTION_DIR"/*.kt "$CONVENTION_DIR"/zed/rainxch/githubstore/convention/*.kt; do
101+
[ -f "$f" ] || continue
102+
sed -i \
103+
-e 's|configureAndroidTarget()|// configureAndroidTarget()|g' \
104+
-e 's|configureKotlinAndroid(this)|// configureKotlinAndroid(this)|g' \
105+
"$f"
106+
done
107+
108+
# ─────────────────────────────────────────────────────────────────────
109+
# 3. KotlinMultiplatform.kt — skip Android configuration
110+
# ─────────────────────────────────────────────────────────────────────
111+
echo "[5/6] Patching KotlinMultiplatform.kt"
112+
cat > "$CONVENTION_DIR/zed/rainxch/githubstore/convention/KotlinMultiplatform.kt" << 'KOTLIN'
113+
package zed.rainxch.githubstore.convention
114+
115+
import org.gradle.api.Project
116+
import org.gradle.kotlin.dsl.configure
117+
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
118+
119+
internal fun Project.configureKotlinMultiplatform() {
120+
// Android target disabled for Flatpak build
121+
configureJvmTarget()
122+
123+
extensions.configure<KotlinMultiplatformExtension> {
124+
compilerOptions {
125+
freeCompilerArgs.add("-Xexpect-actual-classes")
126+
freeCompilerArgs.add("-Xmulti-dollar-interpolation")
127+
freeCompilerArgs.add("-opt-in=kotlin.RequiresOptIn")
128+
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
129+
}
130+
}
131+
}
132+
KOTLIN
133+
134+
# ─────────────────────────────────────────────────────────────────────
135+
# 4. Module build.gradle.kts files — remove android {} blocks
136+
# ─────────────────────────────────────────────────────────────────────
137+
echo "[6/6] Removing android {} blocks from module build.gradle.kts files"
138+
139+
# composeApp — remove android {} block and its contents
140+
python3 -c "
141+
import re, sys
142+
143+
with open('composeApp/build.gradle.kts', 'r') as f:
144+
content = f.read()
145+
146+
# Remove top-level android { ... } blocks (handles nested braces)
147+
def remove_block(text, keyword):
148+
result = []
149+
i = 0
150+
while i < len(text):
151+
# Look for 'android {' at line start (possibly with whitespace)
152+
line_start = text.rfind('\n', 0, i) + 1
153+
prefix = text[line_start:i].strip()
154+
if text[i:].startswith(keyword + ' {') or text[i:].startswith(keyword + '{'):
155+
if prefix == '' or prefix.endswith('\n'):
156+
# Find matching closing brace
157+
brace_start = text.index('{', i)
158+
depth = 1
159+
j = brace_start + 1
160+
while j < len(text) and depth > 0:
161+
if text[j] == '{': depth += 1
162+
elif text[j] == '}': depth -= 1
163+
j += 1
164+
# Skip past the block and any trailing newline
165+
if j < len(text) and text[j] == '\n':
166+
j += 1
167+
i = j
168+
continue
169+
result.append(text[i])
170+
i += 1
171+
return ''.join(result)
172+
173+
content = remove_block(content, 'android')
174+
with open('composeApp/build.gradle.kts', 'w') as f:
175+
f.write(content)
176+
"
177+
178+
# core/data — remove android {} block
179+
for gradle_file in \
180+
core/data/build.gradle.kts \
181+
core/domain/build.gradle.kts \
182+
core/presentation/build.gradle.kts; do
183+
if [ -f "$gradle_file" ]; then
184+
python3 -c "
185+
import sys
186+
with open('$gradle_file', 'r') as f:
187+
lines = f.readlines()
188+
result = []
189+
skip_depth = 0
190+
i = 0
191+
while i < len(lines):
192+
stripped = lines[i].strip()
193+
if stripped.startswith('android {') or stripped == 'android{':
194+
skip_depth = 1
195+
i += 1
196+
while i < len(lines) and skip_depth > 0:
197+
for ch in lines[i]:
198+
if ch == '{': skip_depth += 1
199+
elif ch == '}': skip_depth -= 1
200+
i += 1
201+
continue
202+
result.append(lines[i])
203+
i += 1
204+
with open('$gradle_file', 'w') as f:
205+
f.writelines(result)
206+
"
207+
fi
208+
done
209+
210+
# Remove AndroidApplicationComposeConventionPlugin registration attempt
211+
# (it won't compile without AGP)
212+
cat > "$CONVENTION_DIR/AndroidApplicationComposeConventionPlugin.kt" << 'KOTLIN'
213+
import org.gradle.api.Plugin
214+
import org.gradle.api.Project
215+
216+
class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
217+
override fun apply(target: Project) {
218+
// No-op: Android disabled for Flatpak build
219+
}
220+
}
221+
KOTLIN
222+
223+
cat > "$CONVENTION_DIR/AndroidApplicationConventionPlugin.kt" << 'KOTLIN'
224+
import org.gradle.api.Plugin
225+
import org.gradle.api.Project
226+
227+
class AndroidApplicationConventionPlugin : Plugin<Project> {
228+
override fun apply(target: Project) {
229+
// No-op: Android disabled for Flatpak build
230+
}
231+
}
232+
KOTLIN
233+
234+
echo "=== Android targets disabled successfully ==="

packaging/flatpak/githubstore.sh

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/bin/bash
2+
# GitHub Store Flatpak launcher script
3+
# Launches the uber JAR with the bundled JetBrains Runtime
4+
5+
export JAVA_HOME=/app/jre
6+
7+
# Ensure config directory exists
8+
mkdir -p "${XDG_CONFIG_HOME:-$HOME/.config}/githubstore"
9+
mkdir -p "${XDG_DATA_HOME:-$HOME/.local/share}/githubstore"
10+
11+
exec /app/jre/bin/java \
12+
-Djava.awt.headless=false \
13+
-Dawt.useSystemAAFontSettings=on \
14+
-Dswing.aatext=true \
15+
-Djava.util.prefs.userRoot="${XDG_CONFIG_HOME:-$HOME/.config}/githubstore" \
16+
-Dapp.data.dir="${XDG_DATA_HOME:-$HOME/.local/share}/githubstore" \
17+
-Dapp.downloads.dir="${XDG_DOWNLOAD_DIR:-$HOME/Downloads}" \
18+
-jar /app/lib/githubstore.jar "$@"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
[Desktop Entry]
2+
Type=Application
3+
Name=GitHub Store
4+
GenericName=App Store for GitHub Releases
5+
Comment=Browse, download, and manage apps from GitHub releases
6+
Icon=zed.rainxch.githubstore
7+
Exec=githubstore %u
8+
Terminal=false
9+
StartupNotify=true
10+
Categories=Development;PackageManager;
11+
Keywords=GitHub;Apps;Releases;APK;Store;
12+
StartupWMClass=GitHub-Store
13+
MimeType=x-scheme-handler/githubstore;
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<component type="desktop-application">
3+
<id>zed.rainxch.githubstore</id>
4+
<metadata_license>CC0-1.0</metadata_license>
5+
<project_license>Apache-2.0</project_license>
6+
7+
<name>GitHub Store</name>
8+
<summary>A cross-platform app store for GitHub releases</summary>
9+
10+
<description>
11+
<p>
12+
GitHub Store lets you browse, download, and manage applications distributed
13+
through GitHub releases. It brings the convenience of an app store to the
14+
open-source ecosystem.
15+
</p>
16+
<p>Features include:</p>
17+
<ul>
18+
<li>Discover trending, hot, and popular repositories with installable releases</li>
19+
<li>Search and filter repositories by language, topic, and more</li>
20+
<li>Track installed apps and get notified when updates are available</li>
21+
<li>View release notes, changelogs, and repository READMEs with translation support</li>
22+
<li>Secure installs with TOFU signing key verification and provenance attestation checks</li>
23+
<li>Save favourites and starred repositories for quick access</li>
24+
<li>GitHub OAuth authentication for higher API rate limits and personalized experience</li>
25+
<li>Material 3 theming with light/dark mode and 11 language translations</li>
26+
</ul>
27+
</description>
28+
29+
<launchable type="desktop-id">zed.rainxch.githubstore.desktop</launchable>
30+
31+
<screenshots>
32+
<screenshot type="default">
33+
<caption>Home screen showing trending repositories</caption>
34+
<image>https://raw.githubusercontent.com/AshkanAhmady/GitHub-Store/main/screenshots/home.png</image>
35+
</screenshot>
36+
<!-- Add more screenshots as needed -->
37+
</screenshots>
38+
39+
<url type="homepage">https://github.com/AshkanAhmady/GitHub-Store</url>
40+
<url type="bugtracker">https://github.com/AshkanAhmady/GitHub-Store/issues</url>
41+
<url type="vcs-browser">https://github.com/AshkanAhmady/GitHub-Store</url>
42+
43+
<developer id="zed.rainxch">
44+
<name>rainxchzed</name>
45+
</developer>
46+
47+
<content_rating type="oars-1.1"/>
48+
49+
<releases>
50+
<release version="1.6.2" date="2026-03-18">
51+
<description>
52+
<p>Security improvements: package name and signing key validation on updates,
53+
provenance attestation badges, and app-link verification with asset picker.</p>
54+
</description>
55+
</release>
56+
</releases>
57+
58+
<categories>
59+
<category>Development</category>
60+
<category>PackageManager</category>
61+
</categories>
62+
63+
<keywords>
64+
<keyword>GitHub</keyword>
65+
<keyword>releases</keyword>
66+
<keyword>app store</keyword>
67+
<keyword>package manager</keyword>
68+
<keyword>APK</keyword>
69+
</keywords>
70+
71+
<supports>
72+
<control>pointing</control>
73+
<control>keyboard</control>
74+
</supports>
75+
76+
<requires>
77+
<display_length compare="ge">768</display_length>
78+
</requires>
79+
</component>

0 commit comments

Comments
 (0)