Skip to content

Commit a62b76d

Browse files
authored
feat(desktop): add support for appimage (#966)
1 parent 13dbd65 commit a62b76d

File tree

4 files changed

+326
-1
lines changed

4 files changed

+326
-1
lines changed

.github/workflows/desktop_macos_make.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424

2525
- name: Package Exe(Linux)
2626
if: runner.os == 'Linux'
27-
run: ./gradlew copyBrandingToCommonResources packageDeb -Porganization=ooni
27+
run: ./gradlew copyBrandingToCommonResources packageDeb packageAppImage -Porganization=ooni
2828

2929
- name: Upload artifacts
3030
uses: actions/upload-artifact@v4

buildSrc/src/main/kotlin/TaskRegistration.kt

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@ import java.io.File
77
import kotlin.let
88
import ooni.sparkle.SetupSparkleTask
99
import ooni.sparkle.WinSparkleSetupTask
10+
import ooni.appimage.PackageAppImageTask
1011

1112
private fun isMac() = System.getProperty("os.name").lowercase().contains("mac")
1213

14+
private fun isLinux() = System.getProperty("os.name").lowercase().contains("linux")
15+
1316
/**
1417
* Registers all custom tasks for the project
1518
*/
@@ -20,6 +23,7 @@ fun Project.registerTasks(config: AppConfig) {
2023
registerSparkleTask()
2124
registerWinSparkleTask()
2225
registerOONIDistributableTask()
26+
registerAppImageTask()
2327
configureTaskDependencies()
2428
}
2529

@@ -91,6 +95,31 @@ private fun Project.registerWinSparkleTask() {
9195
}
9296
}
9397

98+
private fun Project.registerAppImageTask() {
99+
tasks.register("packageAppImage", PackageAppImageTask::class) {
100+
group = "distribution"
101+
description = "Creates an AppImage for OONI Probe desktop application on Linux"
102+
onlyIf { isLinux() }
103+
104+
// Depend on createDistributable to ensure the app is built first
105+
dependsOn("createDistributable")
106+
107+
108+
// Set script location relative to root project
109+
scriptFile.set(rootProject.layout.projectDirectory.file("scripts/create-appimage.sh"))
110+
111+
// Set project directory to root project
112+
projectDir.set(rootProject.layout.projectDirectory)
113+
114+
// Set default output location - the actual version will be determined at execution time
115+
outputDir.set(
116+
rootProject.layout.projectDirectory.dir(
117+
"composeApp/build/compose/binaries/main/appimage-workspace/"
118+
)
119+
)
120+
}
121+
}
122+
94123
private fun Project.registerOONIDistributableTask() {
95124
tasks.register("createOONIDistributable") {
96125
group = "build"
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package ooni.appimage
2+
3+
import org.gradle.api.DefaultTask
4+
import org.gradle.api.file.DirectoryProperty
5+
import org.gradle.api.file.RegularFileProperty
6+
import org.gradle.api.tasks.InputDirectory
7+
import org.gradle.api.tasks.InputFile
8+
import org.gradle.api.tasks.OutputDirectory
9+
import org.gradle.api.tasks.TaskAction
10+
import org.gradle.process.ExecOperations
11+
import javax.inject.Inject
12+
13+
abstract class PackageAppImageTask : DefaultTask() {
14+
15+
@get:Inject
16+
abstract val execOperations: ExecOperations
17+
18+
/**
19+
* The create-appimage.sh script file
20+
*/
21+
@get:InputFile
22+
abstract val scriptFile: RegularFileProperty
23+
24+
/**
25+
* The project root directory
26+
*/
27+
@get:InputDirectory
28+
abstract val projectDir: DirectoryProperty
29+
30+
/**
31+
* The expected output AppImage file
32+
*/
33+
@get:OutputDirectory
34+
abstract val outputDir: DirectoryProperty
35+
36+
init {
37+
group = "distribution"
38+
description = "Creates an AppImage for OONI Probe desktop application"
39+
40+
// Set default script location
41+
scriptFile.convention(
42+
project.layout.projectDirectory.file("scripts/create-appimage.sh")
43+
)
44+
45+
// Set default project directory
46+
projectDir.convention(project.layout.projectDirectory)
47+
}
48+
49+
@TaskAction
50+
fun createAppImage() {
51+
val script = scriptFile.get().asFile
52+
53+
if (!script.exists()) {
54+
throw IllegalStateException("AppImage creation script not found at: ${script.absolutePath}")
55+
}
56+
57+
if (!script.canExecute()) {
58+
logger.warn("Making create-appimage.sh executable...")
59+
script.setExecutable(true)
60+
}
61+
62+
val args = mutableListOf<String>(script.absolutePath)
63+
64+
logger.lifecycle("Running: ${args.joinToString(" ")}")
65+
66+
execOperations.exec {
67+
workingDir = projectDir.get().asFile
68+
commandLine = args
69+
standardOutput = System.out
70+
errorOutput = System.err
71+
}.assertNormalExitValue()
72+
73+
// Verify output was created
74+
val outDirFile = outputDir.get().asFile
75+
val output = outDirFile.listFiles()?.firstOrNull { it.isFile && it.name.startsWith("OONI-Probe") && it.name.endsWith(".AppImage") }
76+
if (output != null) {
77+
logger.lifecycle("✓ AppImage created successfully: ${output.absolutePath}")
78+
logger.lifecycle(" Size: ${output.length() / (1024 * 1024)} MB")
79+
} else {
80+
logger.warn("Output AppImage not found in expected directory: ${outDirFile.absolutePath}")
81+
logger.warn("Check the appimage-workspace directory for the output")
82+
}
83+
}
84+
}

scripts/create-appimage.sh

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
#!/bin/bash
2+
set -e
3+
4+
# Script to create AppImage from OONI Probe distributable
5+
# Usage: ./scripts/create-appimage.sh [version]
6+
7+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8+
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
9+
10+
# Colors for output (only used when stdout is a TTY)
11+
_IS_TTY=0
12+
if [ -t 1 ]; then
13+
_IS_TTY=1
14+
fi
15+
16+
RED='\033[0;31m'
17+
GREEN='\033[0;32m'
18+
YELLOW='\033[1;33m'
19+
NC='\033[0m' # No Color
20+
21+
# Gradle-style logging helpers
22+
# Usage: log.lifecycle "message"; log.info "message"; log.warn "message"; log.error "message"; log.success "message"
23+
log_prefix() {
24+
local level="$1"
25+
printf "%s" "[${level}]"
26+
}
27+
28+
log_print() {
29+
local level="$1"; shift
30+
local color="$1"; shift
31+
local msg="$*"
32+
if [ "${_IS_TTY}" -eq 1 ]; then
33+
printf "%s %b%s%b\n" "$(log_prefix "$level")" "${color}" "${msg}" "${NC}"
34+
else
35+
printf "%s %s\n" "$(log_prefix "$level")" "${msg}"
36+
fi
37+
}
38+
39+
log.lifecycle() { log_print LIFECYCLE "${GREEN}" "$*"; }
40+
log.info() { log_print INFO "" "$*"; }
41+
log.warn() { log_print WARN "${YELLOW}" "$*"; }
42+
log.error() { log_print ERROR "${RED}" "$*"; }
43+
log.success() { log_print SUCCESS "${GREEN}" "$*"; }
44+
# Highlight helper for important single-line values (paths, commands)
45+
log.highlight() {
46+
local msg="$*"
47+
if [ "${_IS_TTY}" -eq 1 ]; then
48+
printf " %b%s%b\n" "${YELLOW}" "${msg}" "${NC}"
49+
else
50+
printf " %s\n" "${msg}"
51+
fi
52+
}
53+
54+
# Configuration
55+
APP_NAME="OONI Probe"
56+
APP_ID="org.ooni.probe"
57+
DESKTOP_FILE_NAME="ooniprobe"
58+
BUILD_DIR="${PROJECT_ROOT}/composeApp/build/compose/binaries/main/app"
59+
DIST_DIR="${BUILD_DIR}/${APP_NAME}"
60+
APPDIR_NAME="OONIProbe.AppDir"
61+
APPIMAGE_TOOL_URL="https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
62+
63+
# Get version from gradle.properties or use parameter
64+
if [ -n "$1" ]; then
65+
VERSION="$1"
66+
else
67+
VERSION_FILE="${PROJECT_ROOT}/composeApp/build.gradle.kts"
68+
69+
VERSION=""
70+
71+
# Try to extract versionName from composeApp/build.gradle.kts (supports single/double quotes)
72+
if [ -f "$VERSION_FILE" ]; then
73+
# Use grep -Po to extract the versionName value (handles single or double quotes)
74+
VERSION=$(grep -Po "versionName\s*=\s*['\"]\K[^'\"]+" "$VERSION_FILE" | head -n1 || true)
75+
fi
76+
77+
# Final fallback
78+
if [ -z "$VERSION" ]; then
79+
VERSION="1.0.0"
80+
fi
81+
fi
82+
83+
log.lifecycle "OONI Probe AppImage Creator"
84+
log.info "Version: ${VERSION}"
85+
log.info "Project Root: ${PROJECT_ROOT}"
86+
87+
88+
# Check if distributable exists
89+
if [ ! -d "${DIST_DIR}" ]; then
90+
log.warn "Distributable not found. Building..."
91+
cd "${PROJECT_ROOT}"
92+
./gradlew createDistributable
93+
94+
if [ ! -d "${DIST_DIR}" ]; then
95+
log.error "Error: Failed to create distributable at ${DIST_DIR}"
96+
exit 1
97+
fi
98+
fi
99+
100+
log.success "✓ Distributable found"
101+
102+
# Create workspace
103+
WORKSPACE="${PROJECT_ROOT}/composeApp/build/compose/binaries/main/appimage-workspace"
104+
mkdir -p "${WORKSPACE}"
105+
cd "${WORKSPACE}"
106+
107+
log.lifecycle "Creating AppDir structure..."
108+
109+
# Clean previous AppDir if exists
110+
rm -rf "${APPDIR_NAME}"
111+
mkdir -p "${APPDIR_NAME}/usr"
112+
113+
# Copy application files
114+
log.info "Copying application files..."
115+
cp -r "${DIST_DIR}/bin" "${APPDIR_NAME}/usr/"
116+
cp -r "${DIST_DIR}/lib" "${APPDIR_NAME}/usr/"
117+
118+
# Create AppRun script
119+
log.info "Creating AppRun script..."
120+
cat > "${APPDIR_NAME}/AppRun" << 'EOF'
121+
#!/bin/bash
122+
# AppRun script for OONI Probe
123+
124+
# Get the directory where the AppImage is mounted
125+
HERE="$(dirname "$(readlink -f "${0}")")"
126+
127+
# Set up library paths
128+
export LD_LIBRARY_PATH="${HERE}/usr/lib:${HERE}/usr/lib/runtime/lib:${LD_LIBRARY_PATH}"
129+
export PATH="${HERE}/usr/bin:${PATH}"
130+
131+
# Set up Java-related paths
132+
export JAVA_HOME="${HERE}/usr/lib/runtime"
133+
export PATH="${JAVA_HOME}/bin:${PATH}"
134+
135+
# Launch OONI Probe
136+
exec "${HERE}/usr/bin/OONI Probe" "$@"
137+
EOF
138+
139+
# Create desktop entry
140+
log.info "Creating desktop entry..."
141+
cat > "${APPDIR_NAME}/${DESKTOP_FILE_NAME}.desktop" << EOF
142+
[Desktop Entry]
143+
Type=Application
144+
Name=OONI Probe
145+
GenericName=Network Measurement Tool
146+
Comment=Measure internet censorship and network interference
147+
Exec=OONI Probe %u
148+
Icon=${DESKTOP_FILE_NAME}
149+
Categories=Network;Utility;
150+
Terminal=false
151+
StartupWMClass=OONI Probe
152+
Keywords=censorship;network;measurement;ooni;
153+
MimeType=x-scheme-handler/ooni;
154+
EOF
155+
156+
# Copy icon
157+
log.info "Copying application icon..."
158+
if [ -f "${DIST_DIR}/lib/${APP_NAME}.png" ]; then
159+
cp "${DIST_DIR}/lib/${APP_NAME}.png" "${APPDIR_NAME}/${DESKTOP_FILE_NAME}.png"
160+
elif [ -f "${PROJECT_ROOT}/icons/app.png" ]; then
161+
cp "${PROJECT_ROOT}/icons/app.png" "${APPDIR_NAME}/${DESKTOP_FILE_NAME}.png"
162+
else
163+
log.warn "Warning: No icon found. Using placeholder."
164+
fi
165+
166+
# Also copy icon to standard location
167+
mkdir -p "${APPDIR_NAME}/usr/share/icons/hicolor/256x256/apps"
168+
if [ -f "${APPDIR_NAME}/${DESKTOP_FILE_NAME}.png" ]; then
169+
cp "${APPDIR_NAME}/${DESKTOP_FILE_NAME}.png" "${APPDIR_NAME}/usr/share/icons/hicolor/256x256/apps/${DESKTOP_FILE_NAME}.png"
170+
fi
171+
172+
# Download appimagetool if not present
173+
APPIMAGETOOL="appimagetool-x86_64.AppImage"
174+
if [ ! -f "${APPIMAGETOOL}" ]; then
175+
echo "Downloading appimagetool..."
176+
wget -q --show-progress "${APPIMAGE_TOOL_URL}" -O "${APPIMAGETOOL}"
177+
chmod +x "${APPIMAGETOOL}"
178+
fi
179+
180+
log.success "✓ AppDir created"
181+
182+
# Build AppImage
183+
OUTPUT_NAME="OONI-Probe-${VERSION}-x86_64.AppImage"
184+
log.lifecycle "Building AppImage: ${OUTPUT_NAME}"
185+
186+
ARCH=x86_64 "./${APPIMAGETOOL}" --no-appstream "${APPDIR_NAME}" "${OUTPUT_NAME}"
187+
188+
if [ $? -eq 0 ] && [ -f "${OUTPUT_NAME}" ]; then
189+
echo ""
190+
log.success "AppImage created: ${WORKSPACE}/${OUTPUT_NAME}"
191+
192+
# Make it executable
193+
chmod +x "${OUTPUT_NAME}"
194+
195+
# Get file size
196+
SIZE=$(du -h "${OUTPUT_NAME}" | cut -f1)
197+
log.info "Size: ${SIZE}"
198+
199+
# Calculate SHA256
200+
log.info "Calculating SHA256 checksum..."
201+
sha256sum "${OUTPUT_NAME}" > "${OUTPUT_NAME}.sha256"
202+
log.success "Checksum saved to: ${OUTPUT_NAME}.sha256"
203+
cat "${OUTPUT_NAME}.sha256"
204+
205+
log.info "To test the AppImage, run:"
206+
log.highlight "${WORKSPACE}/${OUTPUT_NAME}"
207+
log.info "To move it to the project dist directory:"
208+
log.highlight "mkdir -p ${PROJECT_ROOT}/dist && mv ${WORKSPACE}/${OUTPUT_NAME}* ${PROJECT_ROOT}/dist/"
209+
else
210+
log.error "Error: Failed to create AppImage"
211+
exit 1
212+
fi

0 commit comments

Comments
 (0)