Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aba3930417 | ||
|
|
4476f41aa6 | ||
|
|
5122683761 | ||
|
|
924835dd55 | ||
|
|
9fd7b02522 | ||
|
|
8e871a3ce6 | ||
|
|
df29546ff9 | ||
|
|
bbd6fe3f5f | ||
|
|
7e87adfc8d | ||
|
|
87729df01f | ||
|
|
18ad29b501 |
+1
-1
@@ -1,4 +1,5 @@
|
||||
.gradle/
|
||||
.ci/
|
||||
build/
|
||||
app/build/
|
||||
local.properties
|
||||
@@ -6,4 +7,3 @@ signing.properties
|
||||
*.jks
|
||||
*.keystore
|
||||
release/
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ val defaultGithubUpdateUrl = providers.gradleProperty("defaultGithubUpdateUrl")
|
||||
.orElse("https://github.com/${repoSlug.get()}/releases/latest/download/latest.json")
|
||||
val defaultGiteaUpdateUrl = providers.gradleProperty("defaultGiteaUpdateUrl")
|
||||
.orElse("https://gitea.neatcn.com/api/v1/repos/tmux/tmux-browser-android/releases/latest")
|
||||
val defaultPreviewUpdateUrl = providers.gradleProperty("defaultPreviewUpdateUrl")
|
||||
.orElse("https://gitea.neatcn.com/tmux/tmux-browser-android/releases/download/preview/latest.json")
|
||||
val defaultApkUrl = providers.gradleProperty("defaultApkUrl")
|
||||
.orElse("https://github.com/${repoSlug.get()}/releases/latest/download/tmux-android.apk")
|
||||
val defaultReleasePageUrl = providers.gradleProperty("defaultReleasePageUrl")
|
||||
@@ -40,6 +42,7 @@ android {
|
||||
buildConfigField("String", "DEFAULT_UPDATE_URL", "\"${defaultUpdateUrl.get()}\"")
|
||||
buildConfigField("String", "DEFAULT_GITHUB_UPDATE_URL", "\"${defaultGithubUpdateUrl.get()}\"")
|
||||
buildConfigField("String", "DEFAULT_GITEA_UPDATE_URL", "\"${defaultGiteaUpdateUrl.get()}\"")
|
||||
buildConfigField("String", "DEFAULT_PREVIEW_UPDATE_URL", "\"${defaultPreviewUpdateUrl.get()}\"")
|
||||
buildConfigField("String", "DEFAULT_APK_URL", "\"${defaultApkUrl.get()}\"")
|
||||
buildConfigField("String", "DEFAULT_RELEASE_PAGE_URL", "\"${defaultReleasePageUrl.get()}\"")
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.util.Base64;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -14,16 +15,22 @@ import java.util.Arrays;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
final class AppEventSocketClient {
|
||||
private static final long HEARTBEAT_INTERVAL_MS = 15000L;
|
||||
private static final int SOCKET_CONNECT_TIMEOUT_MS = 10000;
|
||||
private static final int SOCKET_READ_TIMEOUT_MS = 45000;
|
||||
|
||||
interface Listener {
|
||||
void onMessage(String text);
|
||||
void onClosed();
|
||||
}
|
||||
|
||||
private final Listener listener;
|
||||
private final Object writeLock = new Object();
|
||||
private Socket socket;
|
||||
private BufferedInputStream input;
|
||||
private BufferedOutputStream output;
|
||||
private volatile boolean closed;
|
||||
private Thread heartbeatThread;
|
||||
|
||||
AppEventSocketClient(Listener listener) {
|
||||
this.listener = listener;
|
||||
@@ -40,12 +47,11 @@ final class AppEventSocketClient {
|
||||
sendFrame(8, new byte[0]);
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
try {
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
closeSocketQuietly();
|
||||
}
|
||||
|
||||
boolean isClosed() {
|
||||
return closed;
|
||||
}
|
||||
|
||||
private void run(String baseUrl) {
|
||||
@@ -55,6 +61,7 @@ final class AppEventSocketClient {
|
||||
input = new BufferedInputStream(socket.getInputStream());
|
||||
output = new BufferedOutputStream(socket.getOutputStream());
|
||||
handshake(uri);
|
||||
startHeartbeat();
|
||||
while (!closed) {
|
||||
Frame frame = readFrame();
|
||||
if (frame.opcode == 1) {
|
||||
@@ -69,12 +76,7 @@ final class AppEventSocketClient {
|
||||
} finally {
|
||||
closed = true;
|
||||
listener.onClosed();
|
||||
try {
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
closeSocketQuietly();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,10 +93,39 @@ final class AppEventSocketClient {
|
||||
if (port == -1) {
|
||||
port = "wss".equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
|
||||
}
|
||||
Socket raw = new Socket();
|
||||
raw.connect(new InetSocketAddress(uri.getHost(), port), SOCKET_CONNECT_TIMEOUT_MS);
|
||||
Socket connected;
|
||||
if ("wss".equalsIgnoreCase(uri.getScheme())) {
|
||||
return SSLSocketFactory.getDefault().createSocket(uri.getHost(), port);
|
||||
connected = ((SSLSocketFactory) SSLSocketFactory.getDefault())
|
||||
.createSocket(raw, uri.getHost(), port, true);
|
||||
} else {
|
||||
connected = raw;
|
||||
}
|
||||
return new Socket(uri.getHost(), port);
|
||||
connected.setKeepAlive(true);
|
||||
connected.setTcpNoDelay(true);
|
||||
connected.setSoTimeout(SOCKET_READ_TIMEOUT_MS);
|
||||
return connected;
|
||||
}
|
||||
|
||||
private void startHeartbeat() {
|
||||
heartbeatThread = new Thread(() -> {
|
||||
while (!closed) {
|
||||
try {
|
||||
Thread.sleep(HEARTBEAT_INTERVAL_MS);
|
||||
if (!closed) {
|
||||
sendFrame(9, new byte[0]);
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
} catch (Exception error) {
|
||||
closeSocketQuietly();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, "app-events-ws-heartbeat");
|
||||
heartbeatThread.start();
|
||||
}
|
||||
|
||||
private void handshake(URI uri) throws Exception {
|
||||
@@ -181,6 +212,7 @@ final class AppEventSocketClient {
|
||||
}
|
||||
|
||||
private void sendFrame(int opcode, byte[] payload) throws Exception {
|
||||
synchronized (writeLock) {
|
||||
if (output == null) {
|
||||
return;
|
||||
}
|
||||
@@ -208,6 +240,20 @@ final class AppEventSocketClient {
|
||||
output.write(masked);
|
||||
output.flush();
|
||||
}
|
||||
}
|
||||
|
||||
private void closeSocketQuietly() {
|
||||
if (heartbeatThread != null) {
|
||||
heartbeatThread.interrupt();
|
||||
heartbeatThread = null;
|
||||
}
|
||||
try {
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Frame {
|
||||
final int opcode;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@ import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.Socket;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@@ -19,6 +20,10 @@ import java.util.concurrent.RejectedExecutionException;
|
||||
import javax.net.ssl.SSLSocketFactory;
|
||||
|
||||
final class TerminalSocketClient {
|
||||
private static final long HEARTBEAT_INTERVAL_MS = 15000L;
|
||||
private static final int SOCKET_CONNECT_TIMEOUT_MS = 10000;
|
||||
private static final int SOCKET_READ_TIMEOUT_MS = 45000;
|
||||
|
||||
interface Listener {
|
||||
void onConnected();
|
||||
void onOutput(String data);
|
||||
@@ -35,6 +40,7 @@ final class TerminalSocketClient {
|
||||
private BufferedOutputStream output;
|
||||
private volatile boolean closed;
|
||||
private Thread thread;
|
||||
private Thread heartbeatThread;
|
||||
|
||||
TerminalSocketClient(Listener listener) {
|
||||
this.listener = listener;
|
||||
@@ -97,6 +103,7 @@ final class TerminalSocketClient {
|
||||
"rows", rows
|
||||
);
|
||||
listener.onConnected();
|
||||
startHeartbeat();
|
||||
readLoop();
|
||||
} catch (Exception error) {
|
||||
if (!closed) {
|
||||
@@ -123,10 +130,39 @@ final class TerminalSocketClient {
|
||||
if (port == -1) {
|
||||
port = "wss".equalsIgnoreCase(uri.getScheme()) ? 443 : 80;
|
||||
}
|
||||
Socket raw = new Socket();
|
||||
raw.connect(new InetSocketAddress(uri.getHost(), port), SOCKET_CONNECT_TIMEOUT_MS);
|
||||
Socket connected;
|
||||
if ("wss".equalsIgnoreCase(uri.getScheme())) {
|
||||
return SSLSocketFactory.getDefault().createSocket(uri.getHost(), port);
|
||||
connected = ((SSLSocketFactory) SSLSocketFactory.getDefault())
|
||||
.createSocket(raw, uri.getHost(), port, true);
|
||||
} else {
|
||||
connected = raw;
|
||||
}
|
||||
return new Socket(uri.getHost(), port);
|
||||
connected.setKeepAlive(true);
|
||||
connected.setTcpNoDelay(true);
|
||||
connected.setSoTimeout(SOCKET_READ_TIMEOUT_MS);
|
||||
return connected;
|
||||
}
|
||||
|
||||
private void startHeartbeat() {
|
||||
heartbeatThread = new Thread(() -> {
|
||||
while (!closed) {
|
||||
try {
|
||||
Thread.sleep(HEARTBEAT_INTERVAL_MS);
|
||||
if (!closed) {
|
||||
sendFrame(9, new byte[0]);
|
||||
}
|
||||
} catch (InterruptedException ignored) {
|
||||
Thread.currentThread().interrupt();
|
||||
return;
|
||||
} catch (Exception error) {
|
||||
closeSocketQuietly();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, "terminal-ws-heartbeat");
|
||||
heartbeatThread.start();
|
||||
}
|
||||
|
||||
private void handshake(URI uri) throws Exception {
|
||||
@@ -279,6 +315,10 @@ final class TerminalSocketClient {
|
||||
}
|
||||
|
||||
private void closeSocketQuietly() {
|
||||
if (heartbeatThread != null) {
|
||||
heartbeatThread.interrupt();
|
||||
heartbeatThread = null;
|
||||
}
|
||||
try {
|
||||
if (socket != null) {
|
||||
socket.close();
|
||||
|
||||
@@ -80,6 +80,14 @@ final class UpdateManager {
|
||||
);
|
||||
}
|
||||
|
||||
void checkPreview(boolean userInitiated) {
|
||||
startUpdateCheck(
|
||||
userInitiated,
|
||||
"Preview",
|
||||
new String[]{BuildConfig.DEFAULT_PREVIEW_UPDATE_URL}
|
||||
);
|
||||
}
|
||||
|
||||
void checkWithFallback(boolean userInitiated) {
|
||||
startUpdateCheck(
|
||||
userInitiated,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="AppTheme" parent="android:style/Theme.Material.Light.NoActionBar">
|
||||
<style name="AppTheme" parent="android:style/Theme.Material.NoActionBar">
|
||||
<item name="android:fontFamily">sans</item>
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:statusBarColor">#111418</item>
|
||||
<item name="android:navigationBarColor">#111418</item>
|
||||
<item name="android:windowLightNavigationBar">false</item>
|
||||
<item name="android:statusBarColor">#090B0D</item>
|
||||
<item name="android:navigationBarColor">#090B0D</item>
|
||||
<item name="android:windowBackground">#090B0D</item>
|
||||
<item name="android:windowActionModeOverlay">true</item>
|
||||
</style>
|
||||
</resources>
|
||||
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
# Preview APK workflow
|
||||
|
||||
Use this path for fast UI testing before a formal tag/release.
|
||||
|
||||
The preview build is a debug APK with package id `com.neatstudio.tmuxandroid.debug`.
|
||||
It can be installed next to the formal release app, so preview version codes do not
|
||||
block future formal releases.
|
||||
|
||||
## Build locally
|
||||
|
||||
```bash
|
||||
scripts/setup-android-local.sh
|
||||
scripts/build-preview-apk.sh
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
- `release/preview/tmux-android-preview.apk`
|
||||
- `release/preview/latest.json`
|
||||
|
||||
## Upload to Gitea preview release
|
||||
|
||||
```bash
|
||||
scripts/upload-gitea-preview.sh
|
||||
```
|
||||
|
||||
The script reads `TMUX_GITEA_TOKEN` or prompts for a hidden token.
|
||||
Before uploading, it deletes existing assets with the same names, so the preview
|
||||
release keeps only one current APK and one current manifest.
|
||||
|
||||
Fixed preview URLs:
|
||||
|
||||
- Manifest: `https://gitea.neatcn.com/tmux/tmux-browser-android/releases/download/preview/latest.json`
|
||||
- APK: `https://gitea.neatcn.com/tmux/tmux-browser-android/releases/download/preview/tmux-android-preview.apk`
|
||||
|
||||
## Notes
|
||||
|
||||
- Preview APKs are not formal releases and should not be tagged as `v*`.
|
||||
- Preview uses debug signing unless a separate debug signing setup is added.
|
||||
- The installed preview app is separate from the release app because Gradle applies
|
||||
`applicationIdSuffix = ".debug"` for debug builds.
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CI_DIR="${ROOT_DIR}/.ci"
|
||||
GRADLE_VERSION="${GRADLE_VERSION:-8.10.2}"
|
||||
GRADLE_HOME="${CI_DIR}/gradle-${GRADLE_VERSION}"
|
||||
ANDROID_HOME="${ANDROID_HOME:-${CI_DIR}/android-sdk}"
|
||||
CMDLINE_TOOLS="${ANDROID_HOME}/cmdline-tools/latest"
|
||||
BUILD_NUMBER="${BUILD_NUMBER:-$(date -u +%m%d%H%M)}"
|
||||
VERSION_CODE="${VERSION_CODE:-$((900000000 + 10#${BUILD_NUMBER}))}"
|
||||
VERSION_NAME="${VERSION_NAME:-preview-${BUILD_NUMBER}}"
|
||||
|
||||
if [ ! -x "${GRADLE_HOME}/bin/gradle" ] || [ ! -x "${CMDLINE_TOOLS}/bin/sdkmanager" ]; then
|
||||
"${ROOT_DIR}/scripts/setup-android-local.sh"
|
||||
fi
|
||||
|
||||
export ANDROID_HOME
|
||||
export ANDROID_SDK_ROOT="${ANDROID_HOME}"
|
||||
export PATH="${GRADLE_HOME}/bin:${CMDLINE_TOOLS}/bin:${ANDROID_HOME}/platform-tools:${PATH}"
|
||||
|
||||
cd "${ROOT_DIR}"
|
||||
gradle :app:assembleDebug \
|
||||
-PversionCode="${VERSION_CODE}" \
|
||||
-PversionName="${VERSION_NAME}" \
|
||||
-PrepoSlug="neatstudio/tmux-browser-android"
|
||||
|
||||
OUT_DIR="${ROOT_DIR}/release/preview"
|
||||
mkdir -p "${OUT_DIR}"
|
||||
APK_PATH="$(find app/build/outputs/apk/debug -name '*.apk' | sort | tail -n 1)"
|
||||
cp "${APK_PATH}" "${OUT_DIR}/tmux-android-preview.apk"
|
||||
SHA256="$(sha256sum "${OUT_DIR}/tmux-android-preview.apk" | cut -d " " -f 1)"
|
||||
|
||||
cat > "${OUT_DIR}/latest.json" <<JSON
|
||||
{
|
||||
"versionCode": ${VERSION_CODE},
|
||||
"versionName": "${VERSION_NAME}",
|
||||
"apkUrl": "https://gitea.neatcn.com/tmux/tmux-browser-android/releases/download/preview/tmux-android-preview.apk",
|
||||
"sha256": "${SHA256}",
|
||||
"releasePageUrl": "https://gitea.neatcn.com/tmux/tmux-browser-android/releases/tag/preview",
|
||||
"minSdk": 26
|
||||
}
|
||||
JSON
|
||||
|
||||
ls -lh "${OUT_DIR}/tmux-android-preview.apk" "${OUT_DIR}/latest.json"
|
||||
sha256sum "${OUT_DIR}/tmux-android-preview.apk"
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage: scripts/mirror-gitea-release.sh TAG APK_PATH [VERSION_CODE] [VERSION_NAME]
|
||||
|
||||
Mirrors an already-built APK to the Gitea release for TAG and uploads a
|
||||
Gitea-specific latest.json.
|
||||
|
||||
Token source:
|
||||
TMUX_GITEA_TOKEN environment variable, or hidden stdin prompt.
|
||||
EOF
|
||||
}
|
||||
|
||||
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$#" -lt 2 ] || [ "$#" -gt 4 ]; then
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
|
||||
TAG="$1"
|
||||
APK_PATH="$2"
|
||||
VERSION_NAME="${4:-${TAG#v}}"
|
||||
|
||||
if [ ! -f "$APK_PATH" ]; then
|
||||
echo "APK not found: $APK_PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -n "${3:-}" ]; then
|
||||
VERSION_CODE="$3"
|
||||
else
|
||||
IFS=. read -r MAJOR MINOR PATCH <<EOF
|
||||
$VERSION_NAME
|
||||
EOF
|
||||
VERSION_CODE=$((MAJOR * 1000000 + MINOR * 1000 + PATCH))
|
||||
fi
|
||||
|
||||
TOKEN="${TMUX_GITEA_TOKEN:-}"
|
||||
if [ -z "$TOKEN" ]; then
|
||||
printf "Gitea token: " >&2
|
||||
IFS= read -rs TOKEN
|
||||
printf "\n" >&2
|
||||
fi
|
||||
|
||||
if [ -z "$TOKEN" ]; then
|
||||
echo "Missing Gitea token." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
API_ROOT="https://gitea.neatcn.com/api/v1/repos/tmux/tmux-browser-android"
|
||||
RELEASE_PAGE_URL="https://gitea.neatcn.com/tmux/tmux-browser-android/releases/tag/${TAG}"
|
||||
APK_URL="https://gitea.neatcn.com/tmux/tmux-browser-android/releases/download/${TAG}/tmux-android.apk"
|
||||
SHA256="$(sha256sum "$APK_PATH" | cut -d " " -f 1)"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
LATEST_JSON="${WORK_DIR}/latest.json"
|
||||
cat > "$LATEST_JSON" <<JSON
|
||||
{
|
||||
"versionCode": ${VERSION_CODE},
|
||||
"versionName": "${VERSION_NAME}",
|
||||
"apkUrl": "${APK_URL}",
|
||||
"sha256": "${SHA256}",
|
||||
"releasePageUrl": "${RELEASE_PAGE_URL}",
|
||||
"minSdk": 26
|
||||
}
|
||||
JSON
|
||||
|
||||
RELEASE_JSON="${WORK_DIR}/release.json"
|
||||
BODY='{"tag_name":"'"${TAG}"'","target_commitish":"main","name":"tmux Android '"${VERSION_NAME}"'","body":"Android APK for tmux-ui remote testing.","draft":false,"prerelease":false}'
|
||||
|
||||
if ! curl -fsSL \
|
||||
-X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$BODY" \
|
||||
"${API_ROOT}/releases" \
|
||||
-o "$RELEASE_JSON"; then
|
||||
curl -fsSL \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${API_ROOT}/releases/tags/${TAG}" \
|
||||
-o "$RELEASE_JSON"
|
||||
fi
|
||||
|
||||
RELEASE_ID="$(sed -n 's/^{"id":\([0-9][0-9]*\),.*/\1/p' "$RELEASE_JSON" | head -1)"
|
||||
if [ -z "$RELEASE_ID" ]; then
|
||||
echo "Cannot resolve Gitea release id for ${TAG}." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
upload_asset() {
|
||||
local file="$1"
|
||||
local name="$2"
|
||||
local type="$3"
|
||||
curl -fsSL \
|
||||
-X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-F "attachment=@${file};type=${type}" \
|
||||
"${API_ROOT}/releases/${RELEASE_ID}/assets?name=${name}" \
|
||||
-o "${WORK_DIR}/${name}.asset.json"
|
||||
}
|
||||
|
||||
upload_asset "$APK_PATH" "tmux-android.apk" "application/vnd.android.package-archive"
|
||||
upload_asset "$LATEST_JSON" "latest.json" "application/json"
|
||||
|
||||
echo "Mirrored ${TAG} to Gitea release ${RELEASE_ID}"
|
||||
echo "versionCode=${VERSION_CODE}"
|
||||
echo "versionName=${VERSION_NAME}"
|
||||
echo "sha256=${SHA256}"
|
||||
Executable
+39
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CI_DIR="${ROOT_DIR}/.ci"
|
||||
GRADLE_VERSION="${GRADLE_VERSION:-8.10.2}"
|
||||
ANDROID_TOOLS_ZIP="${ANDROID_TOOLS_ZIP:-commandlinetools-linux-11076708_latest.zip}"
|
||||
GRADLE_HOME="${CI_DIR}/gradle-${GRADLE_VERSION}"
|
||||
ANDROID_HOME="${ANDROID_HOME:-${CI_DIR}/android-sdk}"
|
||||
CMDLINE_TOOLS="${ANDROID_HOME}/cmdline-tools/latest"
|
||||
|
||||
mkdir -p "${CI_DIR}"
|
||||
|
||||
if [ ! -x "${GRADLE_HOME}/bin/gradle" ]; then
|
||||
curl -fSL --connect-timeout 20 --retry 3 --retry-delay 2 --max-time 600 \
|
||||
-o "${CI_DIR}/gradle.zip" \
|
||||
"https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip"
|
||||
unzip -q "${CI_DIR}/gradle.zip" -d "${CI_DIR}"
|
||||
fi
|
||||
|
||||
if [ ! -x "${CMDLINE_TOOLS}/bin/sdkmanager" ]; then
|
||||
mkdir -p "${ANDROID_HOME}/cmdline-tools"
|
||||
curl -fSL --connect-timeout 20 --retry 3 --retry-delay 2 --max-time 600 \
|
||||
-o "${CI_DIR}/android-tools.zip" \
|
||||
"https://dl.google.com/android/repository/${ANDROID_TOOLS_ZIP}"
|
||||
unzip -q "${CI_DIR}/android-tools.zip" -d "${ANDROID_HOME}/cmdline-tools"
|
||||
rm -rf "${CMDLINE_TOOLS}"
|
||||
mv "${ANDROID_HOME}/cmdline-tools/cmdline-tools" "${CMDLINE_TOOLS}"
|
||||
fi
|
||||
|
||||
export ANDROID_HOME
|
||||
export ANDROID_SDK_ROOT="${ANDROID_HOME}"
|
||||
export PATH="${GRADLE_HOME}/bin:${CMDLINE_TOOLS}/bin:${ANDROID_HOME}/platform-tools:${PATH}"
|
||||
|
||||
yes | sdkmanager --licenses >/dev/null || true
|
||||
sdkmanager "platforms;android-35" "build-tools;35.0.0" "platform-tools"
|
||||
|
||||
echo "ANDROID_HOME=${ANDROID_HOME}"
|
||||
echo "GRADLE_HOME=${GRADLE_HOME}"
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
APK_PATH="${1:-${ROOT_DIR}/release/preview/tmux-android-preview.apk}"
|
||||
LATEST_JSON="${2:-${ROOT_DIR}/release/preview/latest.json}"
|
||||
TAG="preview"
|
||||
API_ROOT="https://gitea.neatcn.com/api/v1/repos/tmux/tmux-browser-android"
|
||||
|
||||
if [ ! -f "${APK_PATH}" ]; then
|
||||
echo "APK not found: ${APK_PATH}" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ ! -f "${LATEST_JSON}" ]; then
|
||||
echo "latest.json not found: ${LATEST_JSON}" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TOKEN="${TMUX_GITEA_TOKEN:-}"
|
||||
if [ -z "${TOKEN}" ]; then
|
||||
printf "Gitea token: " >&2
|
||||
IFS= read -rs TOKEN
|
||||
printf "\n" >&2
|
||||
fi
|
||||
if [ -z "${TOKEN}" ]; then
|
||||
echo "Missing Gitea token." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "${WORK_DIR}"' EXIT
|
||||
|
||||
RELEASE_JSON="${WORK_DIR}/release.json"
|
||||
BODY='{"tag_name":"preview","target_commitish":"main","name":"tmux Android Preview","body":"Mutable preview APK for fast UI testing. This is not a formal release.","draft":false,"prerelease":true}'
|
||||
|
||||
if ! curl -fsSL \
|
||||
-X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "${BODY}" \
|
||||
"${API_ROOT}/releases" \
|
||||
-o "${RELEASE_JSON}"; then
|
||||
curl -fsSL \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
"${API_ROOT}/releases/tags/${TAG}" \
|
||||
-o "${RELEASE_JSON}"
|
||||
fi
|
||||
|
||||
RELEASE_ID="$(sed -n 's/^{"id":\([0-9][0-9]*\),.*/\1/p' "${RELEASE_JSON}" | head -1)"
|
||||
if [ -z "${RELEASE_ID}" ]; then
|
||||
echo "Cannot resolve Gitea release id for preview." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
upload_asset() {
|
||||
local file="$1"
|
||||
local name="$2"
|
||||
local type="$3"
|
||||
local assets_json old_ids old_id
|
||||
assets_json="${WORK_DIR}/assets-${name}.json"
|
||||
curl -fsSL -H "Authorization: token ${TOKEN}" \
|
||||
"${API_ROOT}/releases/${RELEASE_ID}/assets" \
|
||||
-o "${assets_json}"
|
||||
old_ids="$(python3 - "${assets_json}" "${name}" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
with open(sys.argv[1], "r", encoding="utf-8") as handle:
|
||||
assets = json.load(handle)
|
||||
for asset in assets:
|
||||
if asset.get("name") == sys.argv[2]:
|
||||
print(asset.get("id"))
|
||||
PY
|
||||
)"
|
||||
for old_id in ${old_ids}; do
|
||||
curl -fsSL -X DELETE -H "Authorization: token ${TOKEN}" \
|
||||
"${API_ROOT}/releases/${RELEASE_ID}/assets/${old_id}" \
|
||||
-o /dev/null
|
||||
done
|
||||
curl -fsSL \
|
||||
-X POST \
|
||||
-H "Authorization: token ${TOKEN}" \
|
||||
-F "attachment=@${file};type=${type}" \
|
||||
"${API_ROOT}/releases/${RELEASE_ID}/assets?name=${name}" \
|
||||
-o "${WORK_DIR}/${name}.asset.json"
|
||||
}
|
||||
|
||||
upload_asset "${APK_PATH}" "tmux-android-preview.apk" "application/vnd.android.package-archive"
|
||||
upload_asset "${LATEST_JSON}" "latest.json" "application/json"
|
||||
|
||||
echo "Uploaded preview release ${RELEASE_ID}"
|
||||
echo "Manifest: https://gitea.neatcn.com/tmux/tmux-browser-android/releases/download/preview/latest.json"
|
||||
echo "APK: https://gitea.neatcn.com/tmux/tmux-browser-android/releases/download/preview/tmux-android-preview.apk"
|
||||
Reference in New Issue
Block a user