Merge branch 'stable' into stable-android-new

# Conflicts:
#	.github/workflows/build.yml
#	flake.nix
#	scripts/desktop/build-lib-linux.sh
This commit is contained in:
shum
2026-05-26 07:19:23 +00:00
1341 changed files with 167737 additions and 18892 deletions
+1 -1
View File
@@ -23,5 +23,5 @@ unzip -o "$tmp/libsimplex.zip" -d "$tmp/simplex-chat/apps/multiplatform/common/s
curl -sSf "$libsup" -o "$tmp/libsupport.zip"
unzip -o "$tmp/libsupport.zip" -d "$tmp/simplex-chat/apps/multiplatform/common/src/commonMain/cpp/android/libs/arm64-v8a"
gradle -p "$tmp/simplex-chat/apps/multiplatform/" clean build
gradle -p "$tmp/simplex-chat/apps/multiplatform/" -Psimplex.assets.dir=../../assets clean build
cp "$tmp/simplex-chat/apps/multiplatform/android/build/outputs/apk/release/android-release-unsigned.apk" "$PWD/simplex-chat.apk"
+17 -9
View File
@@ -67,13 +67,13 @@ checks() {
if ! command -v "$i" > /dev/null 2>&1; then
commands_failed="$i $commands_failed"
else
gradle_ver_local="$(gradle -v | grep Gradle | awk '{print $2}')"
gradle_ver_local_compare="$(printf ${gradle_ver_local:-0.0} | awk -F. '{print $1$2}')"
gradle_ver_local="$(gradle --version | sed -n 's/^Gradle //p')"
gradle_ver_local_compare="$(printf '%s' "$gradle_ver_local" | awk -F. '{print $1"."$2}')"
gradle_ver_remote="$(grep distributionUrl ${folder}/apps/multiplatform/gradle/wrapper/gradle-wrapper.properties)"
gradle_ver_remote="${gradle_ver_remote#*-}"
gradle_ver_remote="${gradle_ver_remote%-*}"
gradle_ver_remote_compare="$(printf ${gradle_ver_remote} | awk -F. '{print $1$2}')"
gradle_ver_remote_compare="$(printf '%s' "$gradle_ver_remote" | awk -F. '{print $1"."$2}')"
if [ "$gradle_ver_local_compare" != "$gradle_ver_remote_compare" ]; then
commands_failed="$i[installed=${gradle_ver_local},required=${gradle_ver_remote}] $commands_failed"
fi
@@ -102,6 +102,7 @@ build() {
sed -i.bak 's/jniLibs.useLegacyPackaging =.*/jniLibs.useLegacyPackaging = true/' "$folder/apps/multiplatform/android/build.gradle.kts"
sed -i.bak '/android {/a lint {abortOnError = false}' "$folder/apps/multiplatform/android/build.gradle.kts"
sed -i.bak '/tasks/Q' "$folder/apps/multiplatform/android/build.gradle.kts"
sed -i.bak "s/android.version_code=.*/android.version_code=${vercode}/" "$folder/apps/multiplatform/gradle.properties"
for arch in $arches; do
if [ "$arch" = "armv7a" ]; then
@@ -133,15 +134,20 @@ build() {
# Build only one arch
sed -i.bak "s/include(.*/include(\"${android_arch}\")/" "$folder/apps/multiplatform/android/build.gradle.kts"
gradle -p "$folder/apps/multiplatform/" clean :android:assembleRelease
gradle -p "$folder/apps/multiplatform/" -Psimplex.assets.dir=../../assets clean :android:assembleRelease
mkdir -p "$android_tmp_folder"
unzip -oqd "$android_tmp_folder" "$android_apk_output"
# Determenistic build
find "$android_tmp_folder" -type f -exec chmod 644 {} +
find "$android_tmp_folder" -type d -exec chmod 755 {} +
find "$android_tmp_folder" -exec touch -h -d '@1764547200' {} +
(
cd "$android_tmp_folder" && \
zip -rq5 "$tmp/$android_apk_output_final" . && \
zip -rq0 "$tmp/$android_apk_output_final" resources.arsc res
find . -type f -print0 | sort -z | xargs -0 zip -X -rq5 "$tmp/$android_apk_output_final" && \
find res resources.arsc -type f -print0 | sort -z | xargs -0 zip -X -rq0 "$tmp/$android_apk_output_final"
)
zipalign -p -f 4 "$tmp/$android_apk_output_final" "$PWD/$android_apk_output_final"
@@ -164,8 +170,10 @@ pre() {
done
shift $(( $OPTIND - 1 ))
commit="${1:-HEAD}"
vercode="${1}"
commit="${2:-HEAD}"
}
main() {
+30 -7
View File
@@ -29,20 +29,43 @@ for ORIG_NAME in "${ORIG_NAMES[@]}"; do
ORIG_NAME_COPY=$ORIG_NAME-copy
mv "$ORIG_NAME" "$ORIG_NAME_COPY"
(cd apk && zip -r -q -"$level" ../"$ORIG_NAME" .)
# Shouldn't be compressed because of Android requirement
(cd apk && zip -r -q -0 ../"$ORIG_NAME" resources.arsc)
# Determenistic build
find apk -type f -exec chmod 644 {} +
find apk -type d -exec chmod 755 {} +
find apk -exec touch -h -d '2025-12-01T00:00:00' {} +
(
cd apk
find . -not -path './res/*' -not -name 'resources.arsc' -type f -print0 | sort -z | xargs -0 zip -X -r -q -"$level" ../"$ORIG_NAME"
)
if [ $case_insensitive -eq 1 ]; then
# For case-insensitive file systems
list_of_files=$(unzip -l "$ORIG_NAME_COPY" | grep res/ | sed -e "s|.*res/|res/|")
for file in $list_of_files; do unzip -o -q -d apk "$ORIG_NAME_COPY" "$file" && (cd apk && zip -r -q -0 ../"$ORIG_NAME" "$file"); done
list_of_files=$(unzip -l "$ORIG_NAME_COPY" | grep res/ | sed -e "s|.*res/|res/|" | sort)
for file in $list_of_files; do
unzip -o -q -d apk "$ORIG_NAME_COPY" "$file"
(
cd apk
chmod 644 "$file"
touch -h -d '2025-12-01T00:00:00' "$file"
zip -X -r -q -0 ../"$ORIG_NAME" "$file"
)
done
else
# This method is not working correctly on case-insensitive file systems since Android AAPT produce the same names of files
# but with different case like xX.png, Xx.png, xx.png, etc
(cd apk && zip -r -q -0 ../"$ORIG_NAME" res)
(
cd apk
find res -type f -print0 | sort -z | xargs -0 zip -X -r -q -0 ../"$ORIG_NAME"
)
fi
# Shouldn't be compressed because of Android requirement
(
cd apk
find resources.arsc -type f -print0 | sort -z | xargs -0 zip -X -r -q -0 ../"$ORIG_NAME"
)
#(cd apk && 7z a -r -mx=$level -tzip -x!resources.arsc ../$ORIG_NAME .)
#(cd apk && 7z a -r -mx=0 -tzip ../$ORIG_NAME resources.arsc)
@@ -61,4 +84,4 @@ for ORIG_NAME in "${ORIG_NAMES[@]}"; do
rm "$ORIG_NAME_COPY" 2> /dev/null || true
rm -rf apk || true
rm "${ORIG_NAME}".idsig 2> /dev/null || true
done
done
+248 -62
View File
@@ -1,96 +1,238 @@
# Transfer data from SQLite to Postgres database
## Transfer data from SQLite to Postgres database
1. \* Decrypt SQLite database if it is encrypted.
1. Decrypt SQLite database if it is encrypted.
```sh
sqlcipher encrypted_simplex_v1_agent.db
```
1. Agent:
```sql
PRAGMA key = 'password';
ATTACH DATABASE 'simplex_v1_agent.db' AS plaintext KEY '';
SELECT sqlcipher_export('plaintext');
DETACH DATABASE plaintext;
```
1. Open sqlite db:
Repeat for `simplex_v1_chat.db`.
```sh
sqlcipher simplex_v1_agent.db
```
2. Run in sqlcipher:
```sql
PRAGMA key = '<your_password>'; -- Set your db password
SELECT count(*) FROM sqlite_master; -- Check if db was successfully decrypted
ATTACH DATABASE 'simplex_v1_agent_plaintext.db' AS plaintext KEY ''; -- Attach new empty db
SELECT sqlcipher_export('plaintext'); -- Export opened db to attached db as plaintext
DETACH DATABASE plaintext;
```
2. Chat:
1. Open sqlite db:
```sh
sqlcipher simplex_v1_chat.db
```
2. Run in sqlcipher:
```sql
PRAGMA key = '<your_password>';
SELECT count(*) FROM sqlite_master;
ATTACH DATABASE 'simplex_v1_chat_plaintext.db' AS plaintext KEY '';
SELECT sqlcipher_export('plaintext');
DETACH DATABASE plaintext;
```
2. Prepare Postgres database.
- Create Postgres database. In shell:
1. Connect to PostgreSQL databse:
```sh
createdb -O simplex simplex_v1
psql -U postgres -h localhost
```
Or via query.
2. Run in psql:
- Build `simplex-chat` executable with `client_postgres` flag and run it to initialize new chat database.
```sql
CREATE USER simplex WITH ENCRYPTED PASSWORD '123123'; -- Create user with password
-- or
-- CREATE USER simplex;
CREATE DATABASE simplex_v1; -- Create database
GRANT ALL PRIVILEGES ON DATABASE simplex_v1 TO simplex; -- Assign permissions
```
This should create `simplex_v1_agent_schema` and `simplex_v1_chat_schema` schemas in `simplex_v1` database, with `migrations` tables populated. Some tables would have initialization data - it will be truncated via pgloader command in next step.
3. Prepare database:
You should build the CLI binary from the same `TAG` as the desktop.
1. Build CLI with PostgreSQL support:
```sh
cabal build -fclient_postgres exe:simplex-chat
```
And rename it to:
```sh
mv simplex-chat simplex-chat-pg
```
2. Execute CLI:
```sh
./simplex-chat-pg -d "postgresql://simplex:123123@localhost:5432/simplex_v1" --create-schema
```
Press `Ctrl+C` when CLI ask for a display name.
This should create `simplex_v1_agent_schema` and `simplex_v1_chat_schema` schemas in `simplex_v1` database, with `migrations` tables populated. Some tables would have initialization data - it will be truncated via pgloader command in next step.
3. Load data from decrypted SQLite databases to Postgres database via pgloader.
Install pgloader and add it to PATH. Run in shell (substitute paths):
```sh
SQLITE_DBPATH='simplex_v1_agent.db' POSTGRES_CONN='postgres://simplex@/simplex_v1' POSTGRES_SCHEMA='simplex_v1_agent_schema' pgloader --on-error-stop sqlite.load
export POSTGRES_CONN='postgresql://simplex:123123@localhost:5432/simplex_v1'
```
SQLITE_DBPATH='simplex_v1_chat.db' POSTGRES_CONN='postgres://simplex@/simplex_v1' POSTGRES_SCHEMA='simplex_v1_chat_schema' pgloader --on-error-stop sqlite.load
And then:
```sh
SQLITE_DBPATH='simplex_v1_agent_plaintext.db' \
POSTGRES_SCHEMA='simplex_v1_agent_schema' \
CPU_CORES=$(nproc) WORKERS=$((CPU_CORES - 1)) pgloader --dynamic-space-size 262144 --on-error-stop sqlite.load
SQLITE_DBPATH='simplex_v1_chat_plaintext.db' \
POSTGRES_SCHEMA='simplex_v1_chat_schema' \
CPU_CORES=$(nproc) WORKERS=$((CPU_CORES - 1)) pgloader --dynamic-space-size 262144 --on-error-stop sqlite.load
```
4. Update sequences for Postgres tables.
```sql
DO $$
DECLARE
rec RECORD;
BEGIN
EXECUTE 'SET SEARCH_PATH TO simplex_v1_agent_schema';
Connect to db:
FOR rec IN
SELECT
table_name,
column_name,
pg_get_serial_sequence(table_name, column_name) AS seq_name
FROM
information_schema.columns
WHERE
table_schema = 'simplex_v1_agent_schema'
AND identity_generation = 'ALWAYS'
LOOP
EXECUTE format(
'SELECT setval(%L, (SELECT MAX(%I) FROM %I))',
rec.seq_name, rec.column_name, rec.table_name
);
END LOOP;
END $$;
```sh
PGPASSWORD=123123 psql -h localhost -U simplex -d simplex_v1
```
Repeat for `simplex_v1_chat_schema`.
Execute the following:
5. \* Compare number of rows between Postgres and SQLite tables.
1. For `agent`:
To check number of rows for all tables in Postgres database schema run:
```sql
DO $$
DECLARE
rec RECORD;
BEGIN
EXECUTE 'SET SEARCH_PATH TO simplex_v1_agent_schema';
```sql
WITH tbl AS (
SELECT table_schema, table_name
FROM information_schema.Tables
WHERE table_name NOT LIKE 'pg_%'
AND table_schema IN ('simplex_v1_agent_schema')
)
SELECT
table_schema AS schema_name,
table_name,
(xpath('/row/c/text()', query_to_xml(
format('SELECT count(*) AS c FROM %I.%I', table_schema, table_name), false, true, ''
)))[1]::text::int AS records_count
FROM tbl
ORDER BY records_count DESC;
```
FOR rec IN
SELECT
table_name,
column_name,
pg_get_serial_sequence(table_name, column_name) AS seq_name
FROM
information_schema.columns
WHERE
table_schema = 'simplex_v1_agent_schema'
AND identity_generation = 'ALWAYS'
LOOP
EXECUTE format(
'SELECT setval(%L, (SELECT MAX(%I) FROM %I))',
rec.seq_name, rec.column_name, rec.table_name
);
END LOOP;
END $$;
```
Repeat for `simplex_v1_chat_schema`.
2. For `chat`:
```sql
DO $$
DECLARE
rec RECORD;
BEGIN
EXECUTE 'SET SEARCH_PATH TO simplex_v1_chat_schema';
FOR rec IN
SELECT
table_name,
column_name,
pg_get_serial_sequence(table_name, column_name) AS seq_name
FROM
information_schema.columns
WHERE
table_schema = 'simplex_v1_chat_schema'
AND identity_generation = 'ALWAYS'
LOOP
EXECUTE format(
'SELECT setval(%L, (SELECT MAX(%I) FROM %I))',
rec.seq_name, rec.column_name, rec.table_name
);
END LOOP;
END $$;
```
5. Compare number of rows between Postgres and SQLite tables.
**PostgreSQL**:
1. For `agent`:
```sql
WITH tbl AS (
SELECT table_schema, table_name
FROM information_schema.Tables
WHERE table_name NOT LIKE 'pg_%'
AND table_schema IN ('simplex_v1_agent_schema')
)
SELECT
table_schema AS schema_name,
table_name,
(xpath('/row/c/text()', query_to_xml(
format('SELECT count(*) AS c FROM %I.%I', table_schema, table_name), false, true, ''
)))[1]::text::int AS records_count
FROM tbl
ORDER BY records_count DESC;
```
2. For `chat`:
```sql
WITH tbl AS (
SELECT table_schema, table_name
FROM information_schema.Tables
WHERE table_name NOT LIKE 'pg_%'
AND table_schema IN ('simplex_v1_chat_schema')
)
SELECT
table_schema AS schema_name,
table_name,
(xpath('/row/c/text()', query_to_xml(
format('SELECT count(*) AS c FROM %I.%I', table_schema, table_name), false, true, ''
)))[1]::text::int AS records_count
FROM tbl
ORDER BY records_count DESC;
```
**SQLite**:
1. For `agent`:
```sh
db="simplex_v1_agent_plaintext.db"
sqlite3 "$db" "SELECT name FROM sqlite_master WHERE type='table';" |
while read table; do
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM \"$table\";")
echo "$table: $count"
done | sort -k2 -nr | less
```
2. For `chat`:
```sh
db="simplex_v1_chat_plaintext.db"
sqlite3 "$db" "SELECT name FROM sqlite_master WHERE type='table';" |
while read table; do
count=$(sqlite3 "$db" "SELECT COUNT(*) FROM \"$table\";")
echo "$table: $count"
done | sort -k2 -nr | less
```
6. Build and run desktop app with Postgres backend.
@@ -103,3 +245,47 @@
# or
./gradlew packageDmg -Pdatabase.backend=postgres
```
## Transfer data from Postgres to SQLite database
1. Prepare sqlite db:
1. Download simplex-chat CLI:
You should download the CLI binary from the same `TAG` as the desktop.
```sh
export TAG='v6.4.3.1'
curl -L "https://github.com/simplex-chat/simplex-chat/releases/download/${TAG}/simplex-chat-ubuntu-22_04-x86_64" -o 'simplex-chat'
```
2. Run the CLI:
```sh
./simplex-chat
```
Press `Ctrl+C` when CLI ask for a display name.
3. Move database:
```sh
mv ~/.simplex/simplex_v1_* ~/.local/share/simplex/
```
2. Transfer data:
```sh
./pg2sqlite.py --verbose 'postgresql://simplex:123123@localhost:5432/simplex_v1' ~/.local/share/simplex/
```
4. Update BLOBs:
```sh
sqlite3 simplex_v1_chat.db
```
```sh
UPDATE group_members SET member_role = CAST(member_role as BLOB);
UPDATE user_contact_links SET group_link_member_role = CAST(group_link_member_role AS BLOB) WHERE group_link_member_role is not null;
```
+899
View File
@@ -0,0 +1,899 @@
#!/usr/bin/env python3
"""
PostgreSQL -> per-schema SQLite migration with colored, clean logging.
Usage example:
python db_migrate.py 'postgresql://user:pass@host:5432/db' /path/to/sqlite/dir --dry-run
Note: color output will be disabled automatically if stdout is not a TTY or if --no-color is passed.
"""
from __future__ import annotations
import argparse
import logging
import os
import sqlite3
import sys
import re
import datetime
from pathlib import Path
from typing import Set, List, Tuple, Dict, Optional, NamedTuple
import psycopg
from psycopg import sql
# ----------------------
# Small utilities
# ----------------------
try:
sqlite3.register_adapter(
datetime.datetime,
lambda v: v.isoformat(sep=" ", timespec="microseconds"),
)
sqlite3.register_adapter(datetime.date, lambda v: v.isoformat())
sqlite3.register_adapter(
datetime.time, lambda v: v.isoformat(timespec="microseconds")
)
except ValueError:
pass # already registered
ANSI = {
"reset": "\x1b[0m",
"bold": "\x1b[1m",
"dim": "\x1b[2m",
"red": "\x1b[31m",
"green": "\x1b[32m",
"yellow": "\x1b[33m",
"blue": "\x1b[34m",
"magenta": "\x1b[35m",
"cyan": "\x1b[36m",
"gray": "\x1b[90m",
}
DEFAULT_BATCH_SIZE = 10000
TYPE_COMPATIBILITY = {
"bytea": ["BLOB", "CHAR", "CLOB", "TEXT", "JSON"],
"int": ["INT", "NUMERIC"],
"serial": ["INT", "NUMERIC"],
"numeric": ["NUMERIC", "DECIMAL", "REAL", "FLOAT", "DOUBLE"],
"decimal": ["NUMERIC", "DECIMAL", "REAL", "FLOAT", "DOUBLE"],
"real": ["NUMERIC", "DECIMAL", "REAL", "FLOAT", "DOUBLE"],
"double": ["NUMERIC", "DECIMAL", "REAL", "FLOAT", "DOUBLE"],
"float": ["NUMERIC", "DECIMAL", "REAL", "FLOAT", "DOUBLE"],
"money": ["NUMERIC", "DECIMAL", "REAL", "FLOAT", "DOUBLE"],
"bool": ["BOOL", "INT", "NUMERIC"],
"varchar": ["CHAR", "CLOB", "TEXT"],
"char": ["CHAR", "CLOB", "TEXT"],
"text": ["CHAR", "CLOB", "TEXT"],
"citext": ["CHAR", "CLOB", "TEXT"],
"timestamp": ["DATE", "TIME", "CHAR", "TEXT", "DATETIME"],
"time": ["DATE", "TIME", "CHAR", "TEXT", "DATETIME"],
"date": ["DATE", "TIME", "CHAR", "TEXT", "DATETIME"],
"uuid": ["CHAR", "TEXT", "UUID", "CLOB"],
"json": ["JSON", "TEXT", "CHAR", "CLOB"],
"jsonb": ["JSON", "TEXT", "CHAR", "CLOB"],
}
def _sanitize_cursor_name(s: str) -> str:
return re.sub(r"[^A-Za-z0-9_]+", "_", s)
def supports_color(force_no: bool) -> bool:
"""Return True when we should emit ANSI colors."""
if force_no:
return False
if os.getenv("NO_COLOR"):
return False
term = os.getenv("TERM", "")
if term == "" or term.lower() == "dumb":
return False
try:
isatty = sys.stdout.isatty()
except Exception:
isatty = False
return isatty
class ColoredFormatter(logging.Formatter):
LEVEL_COLORS = {
logging.DEBUG: ANSI["gray"],
logging.INFO: ANSI["green"],
logging.WARNING: ANSI["yellow"],
logging.ERROR: ANSI["red"],
logging.CRITICAL: ANSI["red"] + ANSI["bold"],
}
TAG_COLORS = {
"SKIP": ANSI["yellow"],
"SCHEMA": ANSI["blue"],
"OK": ANSI["magenta"],
}
def __init__(self, use_color: bool = True):
super().__init__(fmt="%(message)s")
self.use_color = use_color
def format(self, record: logging.LogRecord) -> str:
msg = super().format(record)
parts = msg.split(" ", 1)
tag = parts[0]
rest = parts[1] if len(parts) > 1 else ""
plain_label = f"[{tag}]"
if not self.use_color:
return f"{plain_label}{(' ' + rest) if rest else ''}"
color = self.TAG_COLORS.get(
tag.upper(), self.LEVEL_COLORS.get(record.levelno, "")
)
reset = ANSI["reset"]
return f"{color}{plain_label}{reset}{(' ' + rest) if rest else ''}"
def setup_logger(verbose: bool, no_color: bool) -> logging.Logger:
use_color = supports_color(no_color)
logger = logging.getLogger("db_migrate")
logger.setLevel(logging.DEBUG if verbose else logging.INFO)
handler = logging.StreamHandler()
handler.setLevel(logging.DEBUG if verbose else logging.INFO)
handler.setFormatter(ColoredFormatter(use_color=use_color))
logger.handlers.clear()
logger.addHandler(handler)
logging.getLogger("psycopg").setLevel(logging.WARNING)
if verbose:
logger.debug(f"color_support: {use_color}")
try:
isatty = sys.stdout.isatty()
except Exception:
isatty = False
logger.debug(
f"TERM={os.getenv('TERM', '')!r} "
f"NO_COLOR={os.getenv('NO_COLOR')!r} "
f"isatty={isatty}"
)
return logger
def quote_sqlite_identifier(name: str) -> str:
return '"' + name.replace('"', '""') + '"'
def quote_pg_identifier(name: str) -> str:
"""Simple PG identifier quoting for building safe SQL strings."""
return '"' + name.replace('"', '""') + '"'
def sqlite_decl_satisfies(pg_type: str, sqlite_decl: str) -> bool:
# Treat empty/blank SQLite declarations more permissively based on PG type.
decl_raw = sqlite_decl or ""
if not decl_raw.strip():
pg = (pg_type or "").lower()
# arrays -> textual affinity
if pg.endswith("[]"):
return True
# integer-like
if re.search(r"\b(?:int|serial|bigint)\b", pg):
return True
# numeric/float
if re.search(r"\b(?:numeric|decimal|real|double|float|money)\b", pg):
return True
# boolean
if re.search(r"\b(?:bool|boolean)\b", pg):
return True
# binary
if re.search(r"\b(?:bytea)\b", pg):
return True
# textual/json/uuid/timestamps/dates/times
if re.search(
r"\b(?:varchar|char|text|citext|jsonb|json|uuid|timestamp|time|date)\b", pg
):
return True
# conservative fallback: accept empty decl as permissive
return True
decl = decl_raw.upper()
pg = (pg_type or "").lower()
# array type
if pg.endswith("[]"):
return any(tok in decl for tok in ("TEXT", "CHAR", "CLOB", "JSON"))
for key, allowed_types in TYPE_COMPATIBILITY.items():
if re.search(r"\b" + re.escape(key) + r"\b", pg):
return any(tok in decl for tok in allowed_types)
return any(
tok in decl for tok in ("TEXT", "CHAR", "CLOB", "NUMERIC", "BLOB", "INT")
)
# ----------------------
# Postgres helpers
# ----------------------
def list_user_schemas(pg_cursor) -> List[str]:
pg_cursor.execute(
"""
SELECT nspname
FROM pg_namespace
WHERE nspname NOT LIKE 'pg_%'
AND nspname != 'information_schema'
AND nspname != 'public'
ORDER BY nspname;
"""
)
return [r[0] for r in pg_cursor.fetchall()]
def list_tables_in_schema(pg_cursor, schema: str) -> List[str]:
pg_cursor.execute(
sql.SQL(
"""
SELECT table_name
FROM information_schema.tables
WHERE table_schema = %s
ORDER BY table_name;
"""
),
(schema,),
)
return [r[0] for r in pg_cursor.fetchall()]
def get_pg_columns(pg_cursor, schema: str, table: str) -> List[Tuple[str, str]]:
pg_cursor.execute(
sql.SQL(
"""
SELECT column_name, data_type
FROM information_schema.columns
WHERE table_schema = %s AND table_name = %s
ORDER BY ordinal_position;
"""
),
(schema, table),
)
return [(r[0], r[1]) for r in pg_cursor.fetchall()]
# ----------------------
# SQLite helpers
# ----------------------
class SQLiteCol(NamedTuple):
cid: int
name: str
type: str
notnull: int
dflt_value: str
pk: int
def get_sqlite_table_info(sqlite_cursor, table_name: str) -> list[SQLiteCol]:
qname = quote_sqlite_identifier(table_name)
sqlite_cursor.execute(f"PRAGMA table_info({qname});")
return [SQLiteCol(*row) for row in sqlite_cursor.fetchall()]
def sqlite_table_has_autoincrement(sqlite_cursor, table_name: str) -> bool:
sqlite_cursor.execute(
"SELECT sql FROM sqlite_master WHERE type='table' AND name = ?;", (table_name,)
)
row = sqlite_cursor.fetchone()
if not row or not row[0]:
return False
return "AUTOINCREMENT" in row[0].upper()
def sqlite_sequence_table_exists(sqlite_cursor) -> bool:
sqlite_cursor.execute(
"SELECT name FROM sqlite_master WHERE type='table' AND name='sqlite_sequence';"
)
exists = sqlite_cursor.fetchone() is not None
if not exists:
logging.getLogger("db_migrate").debug("sqlite_sequence table not present")
return exists
# ----------------------
# Core migration logic
# ----------------------
def build_select_for_sqlite_columns(
schema: str,
table: str,
sqlite_cols: list[SQLiteCol],
pg_columns_map: Dict[str, Tuple[str, str]],
) -> str:
"""Return a plain SQL string (no trailing semicolon) selecting PG columns in the order of sqlite_cols.
Uses simple identifier quoting to avoid psycopg.sql objects which may vary by driver version.
"""
select_parts = []
for col in sqlite_cols:
lc = col.name.lower()
if lc in pg_columns_map:
pg_name, pg_type = pg_columns_map[lc]
pg_type_l = (pg_type or "").lower()
if any(tok in pg_type_l for tok in ("timestamp", "time")):
expr = f"{quote_pg_identifier(pg_name)}::text"
else:
expr = f"{quote_pg_identifier(pg_name)}"
select_parts.append(expr)
else:
select_parts.append("NULL")
select_list = ", ".join(select_parts)
# Quote schema and table names simply (this is not comprehensive for every corner case but avoids psycopg.sql use)
return f"SELECT {select_list} FROM {quote_pg_identifier(schema)}.{quote_pg_identifier(table)}"
def validate_column_compatibility(
sqlite_cols: list[SQLiteCol],
pg_columns_map: Dict[str, Tuple[str, str]],
schema: str,
table: str,
) -> None:
for col in sqlite_cols:
lc = col.name.lower()
if lc not in pg_columns_map:
continue
pg_type = pg_columns_map[lc][1]
if not sqlite_decl_satisfies(pg_type, col.type):
raise ValueError(
f"Type mismatch for {schema}.{table}.{col.name}: "
f"PG='{pg_type}' vs SQLite='{col.type}'"
)
def process_row_for_sqlite(row: Tuple, sqlite_cols: list[SQLiteCol]) -> Tuple:
out = []
for i, val in enumerate(row):
col = sqlite_cols[i]
decl_raw = col.type or ""
decl = decl_raw.upper()
decl_is_empty = decl_raw.strip() == ""
if isinstance(val, memoryview):
b = val.tobytes()
if "BLOB" in decl or decl_is_empty:
out.append(sqlite3.Binary(b))
else:
try:
out.append(b.decode("utf-8"))
except UnicodeDecodeError as e:
raise ValueError(
f"UTF-8 decode failed for column '{col.name}': {e}"
)
elif isinstance(val, (bytes, bytearray)):
b = bytes(val)
if "BLOB" in decl or decl_is_empty:
out.append(sqlite3.Binary(b))
else:
try:
out.append(b.decode("utf-8"))
except UnicodeDecodeError as e:
raise ValueError(
f"UTF-8 decode failed for column '{col.name}': {e}"
)
else:
out.append(val)
return tuple(out)
def fetch_and_validate_row_batches(
pg_cursor,
select_sql: str,
sqlite_cols: list[SQLiteCol],
batch_size: int = DEFAULT_BATCH_SIZE,
):
pg_cursor.execute(select_sql)
total_rows_seen = 0
while True:
rows = pg_cursor.fetchmany(batch_size)
if not rows:
break
validated_batch = []
for row in rows:
total_rows_seen += 1
try:
validated_batch.append(process_row_for_sqlite(row, sqlite_cols))
except ValueError as e:
raise ValueError(f"Row {total_rows_seen} validation error: {e}")
yield validated_batch
def _get_postgres_sequence_info(
pg_cursor, schema: str, table: str, pk_col: str
) -> Optional[tuple[str, int]]:
"""Get PostgreSQL sequence name and last_value for a table primary key, if any.
Returns (sequence_name_text, last_value) or None.
"""
pg_cursor.execute(
"""
SELECT data_type
FROM information_schema.columns
WHERE table_schema = %s AND table_name = %s AND column_name = %s;
""",
(schema, table, pk_col),
)
r = pg_cursor.fetchone()
if not r:
return None
pg_type = (r[0] or "").lower()
# Match whole words to avoid matching 'interval' etc.
if not re.search(r"\b(?:int|serial|bigint)\b", pg_type):
return None
# Get sequence name text (NULL if none)
pg_cursor.execute(
"SELECT pg_get_serial_sequence(%s, %s);",
(f"{schema}.{table}", pk_col),
)
row = pg_cursor.fetchone()
seq_name = row[0] if row else None
if not seq_name:
return None
try:
# Read last_value by casting the pg_get_serial_sequence result to regclass.
pg_cursor.execute(
"SELECT last_value FROM pg_get_serial_sequence(%s, %s)::regclass;",
(f"{schema}.{table}", pk_col),
)
rr = pg_cursor.fetchone()
if rr and rr[0] is not None:
return (seq_name, int(rr[0]))
except Exception:
# Swallow errors (best-effort); callers treat missing info as absent
pass
return None
def _get_sqlite_max_pk_value(sqlite_cursor, table: str, pk_col: str) -> Optional[int]:
"""Get the maximum primary key value from SQLite table."""
logger = logging.getLogger("db_migrate")
try:
sqlite_cursor.execute(
f"SELECT MAX({quote_sqlite_identifier(pk_col)}) "
f"FROM {quote_sqlite_identifier(table)};"
)
r3 = sqlite_cursor.fetchone()
if r3 and r3[0] is not None:
return int(r3[0])
except Exception:
logger.debug("Failed to read sqlite max pk", exc_info=True)
return None
def _update_sqlite_sequence(
sqlite_conn: sqlite3.Connection, table: str, sequence_value: int
) -> bool:
"""Update SQLite sqlite_sequence with new value.
IMPORTANT: This function does not commit; caller must commit/rollback.
"""
s_cur = sqlite_conn.cursor()
s_cur.execute(
"UPDATE sqlite_sequence SET seq = ? WHERE name = ?;",
(sequence_value, table),
)
if s_cur.rowcount == 0:
s_cur.execute(
"INSERT INTO sqlite_sequence(name, seq) VALUES (?, ?);",
(table, sequence_value),
)
return True
def try_update_sqlite_sequence(
pg_cursor,
sqlite_conn: sqlite3.Connection,
schema: str,
pg_table: str, # PostgreSQL table name
sqlite_table: str, # SQLite table name
sqlite_cols: list[SQLiteCol],
) -> bool:
"""Update SQLite sequence table based on PostgreSQL sequence values."""
logger = logging.getLogger("db_migrate")
# Check if table has a single primary key
pks = [c for c in sqlite_cols if c.pk]
if len(pks) != 1:
return False
pk_col = pks[0].name
# Check if SQLite has AUTOINCREMENT (use sqlite_table)
s_cur = sqlite_conn.cursor()
if not sqlite_table_has_autoincrement(s_cur, sqlite_table):
return False
if not sqlite_sequence_table_exists(s_cur):
return False
# Get PostgreSQL sequence info (use pg_table)
seq_info = _get_postgres_sequence_info(pg_cursor, schema, pg_table, pk_col)
pg_last_value = seq_info[1] if seq_info else None
# Get SQLite max PK value (use sqlite_table)
sqlite_max = _get_sqlite_max_pk_value(s_cur, sqlite_table, pk_col)
# Determine the value to use
if pg_last_value is not None and sqlite_max is not None:
candidate = max(pg_last_value, sqlite_max)
elif pg_last_value is not None:
candidate = pg_last_value
elif sqlite_max is not None:
candidate = sqlite_max
else:
return False
updated = _update_sqlite_sequence(sqlite_conn, sqlite_table, candidate)
if updated:
try:
sqlite_conn.commit()
except Exception:
logger.debug("Failed to commit sqlite_sequence update", exc_info=True)
# Let caller proceed; treat as best-effort
return updated
# ----------------------
# Flow: per-schema migration
# ----------------------
def migrate_schema(
pg_conn,
sqlite_dir: str,
schema: str,
skipped_tables: Set[str],
logger: logging.Logger,
dry_run: bool = False,
batch_size: int = DEFAULT_BATCH_SIZE,
) -> Tuple[int, int]:
"""Migrate a single schema. Returns (tables_migrated, rows_inserted_total)."""
processed_schema = schema[:-7] if schema.endswith("_schema") else schema
sqlite_db_path = Path(sqlite_dir) / f"{processed_schema}.db"
if not sqlite_db_path.is_file():
logger.error(f"Missing SQLite DB: {sqlite_db_path}")
return 0, 0
tables_migrated = 0
rows_inserted = 0
sqlite_path = sqlite_db_path.resolve()
if dry_run:
uri = sqlite_path.as_uri() + "?mode=ro"
conn_args = {"database": uri, "uri": True}
else:
conn_args = {"database": str(sqlite_path)}
with sqlite3.connect(**conn_args) as sqlite_conn:
sqlite_cur = sqlite_conn.cursor()
sqlite_cur.execute("SELECT name FROM sqlite_master WHERE type='table';")
sqlite_tables = [r[0] for r in sqlite_cur.fetchall()]
# O(1) lookup map for matching by lowercase name
sqlite_table_map = {t.lower(): t for t in sqlite_tables}
with pg_conn.cursor() as pg_cur:
# Ensure we start in a clean state for this connection
try:
pg_conn.rollback()
except Exception:
# Ignore rollback failure; connection is newly opened most likely
logger.debug(
"pg_conn.rollback() at schema start ignored", exc_info=True
)
# Get table list for this schema, but catch/rollback on failure
try:
tables = list_tables_in_schema(pg_cur, schema)
except Exception as e:
logger.error("ERROR %s (list tables): %s", schema, e)
logger.debug("Traceback (list tables):", exc_info=True)
try:
pg_conn.rollback()
except Exception:
logger.debug(
"pg_conn.rollback() failed after list tables error",
exc_info=True,
)
return 0, 0
for table in tables:
# Defensive: ensure connection is in a clean state before any new PG work.
# A prior error can leave the connection in an aborted transaction; calling
# rollback() clears that and allows subsequent SELECTs to run.
try:
pg_conn.rollback()
except Exception:
# ignore: best-effort cleanup
logger.debug(
"pg_conn.rollback() ignored at start of table loop",
exc_info=True,
)
if table.lower() in skipped_tables:
logger.info("SKIP %s.%s (explicit)", schema, table)
continue
if table.lower() not in sqlite_table_map:
logger.info("SKIP %s.%s (no target table)", schema, table)
continue
matched_table = sqlite_table_map[table.lower()]
logger.info("OK %s.%s -> %s", schema, table, matched_table)
# Fetch PG columns, but defend against aborted transaction here
try:
pg_cols = get_pg_columns(pg_cur, schema, table)
except Exception as e:
logger.error("ERROR %s.%s (get columns): %s", schema, table, e)
logger.debug("Traceback (get columns):", exc_info=True)
try:
pg_conn.rollback()
except Exception:
logger.debug(
"pg_conn.rollback() failed after get columns error",
exc_info=True,
)
continue
pg_map: Dict[str, Tuple[str, str]] = {
name.lower(): (name, dtype) for name, dtype in pg_cols
}
sqlite_info = get_sqlite_table_info(sqlite_cur, matched_table)
if not sqlite_info:
logger.warning(f"SKIP {schema}.{table} (no sqlite info)")
continue
# Warn once about schema drift (extra columns)
pg_col_names = {name.lower() for name, _ in pg_cols}
extra_in_sqlite = {
c.name for c in sqlite_info if c.name.lower() not in pg_col_names
}
if extra_in_sqlite:
logger.warning(
f"Schema drift: {schema}.{table} has extra SQLite columns {extra_in_sqlite}"
)
try:
validate_column_compatibility(sqlite_info, pg_map, schema, table)
except ValueError as e:
logger.error(f"ERROR {schema}.{table} (type mismatch): {e}")
continue
select_sql = build_select_for_sqlite_columns(
schema, table, sqlite_info, pg_map
)
csr_name = _sanitize_cursor_name(f"csr_{schema}_{table}")
try:
with pg_conn.cursor(name=csr_name) as data_cur:
batch_gen = fetch_and_validate_row_batches(
data_cur, select_sql, sqlite_info, batch_size=batch_size
)
first_batch = next(batch_gen, None)
if not first_batch:
logger.info(f"SKIP {schema}.{table} (no rows)")
# data_cur will be closed automatically on leaving the 'with'
continue
if dry_run:
count = len(first_batch)
for batch in batch_gen:
count += len(batch)
inserted = 0
logger.info(
f"DRY {schema}.{table} rows_validated={count}"
)
else:
cur = sqlite_conn.cursor()
quoted_table = quote_sqlite_identifier(matched_table)
col_names = [c.name for c in sqlite_info]
quoted_cols = ", ".join(
quote_sqlite_identifier(c) for c in col_names
)
placeholders = ", ".join(["?"] * len(col_names))
cur.execute("BEGIN;")
try:
cur.execute(f"DELETE FROM {quoted_table};")
inserted = 0
if first_batch:
cur.executemany(
f"INSERT INTO {quoted_table} ({quoted_cols}) VALUES ({placeholders});",
first_batch,
)
inserted += len(first_batch)
for batch in batch_gen:
if not batch:
continue
cur.executemany(
f"INSERT INTO {quoted_table} ({quoted_cols}) VALUES ({placeholders});",
batch,
)
inserted += len(batch)
except Exception:
cur.execute("ROLLBACK;")
raise
else:
cur.execute("COMMIT;")
except ValueError as e:
logger.error("ERROR %s.%s (row validation): %s", schema, table, e)
try:
pg_conn.rollback()
except Exception:
logger.debug(
"pg_conn.rollback() failed after row validation error",
exc_info=True,
)
continue
except Exception as e:
logger.error("ERROR %s.%s (select failed): %s", schema, table, e)
logger.debug("Traceback (select failed):", exc_info=True)
try:
pg_conn.rollback()
except Exception:
logger.debug(
"pg_conn.rollback() failed after select failed",
exc_info=True,
)
continue
rows_inserted += inserted
tables_migrated += 1
logger.info(f"DONE {schema}.{table} rows={inserted}")
if not dry_run:
try:
if try_update_sqlite_sequence(
pg_cur,
sqlite_conn,
schema,
table,
matched_table,
sqlite_info,
):
logger.info(
"SEQ %s.%s sqlite_sequence updated", schema, table
)
except Exception as e:
logger.warning(
"SEQ %s.%s update failed (ignored): %s", schema, table, e
)
return tables_migrated, rows_inserted
def migrate_data(
pg_conn_str: str,
sqlite_dir: str,
schema_filter: Optional[str],
dry_run: bool,
skip_tables: str,
logger: logging.Logger,
batch_size: int,
) -> None:
skipped_tables = {t.strip().lower() for t in skip_tables.split(",") if t.strip()}
total_tables = 0
total_rows = 0
total_errors = 0
# List schemas once with a short-lived connection
with psycopg.connect(pg_conn_str) as tmp_conn:
with tmp_conn.cursor() as cur:
schemas = list_user_schemas(cur)
if schema_filter:
schemas = [s for s in schemas if s == schema_filter]
if not schemas:
logger.error("No schemas to process")
return
for schema in schemas:
logger.info("SCHEMA %s", schema)
if dry_run:
logger.info("(dry-run) validating only — no writes will be performed")
# Use a fresh connection per-schema to isolate failures/aborted transactions
try:
with psycopg.connect(pg_conn_str) as pg_conn:
migrated_tables, inserted_rows = migrate_schema(
pg_conn,
sqlite_dir,
schema,
skipped_tables,
logger,
dry_run=dry_run,
batch_size=batch_size,
)
except Exception as e:
logger.error("Schema %s failed: %s", schema, e)
logger.debug("Traceback (schema failure):", exc_info=True)
total_errors += 1
continue
total_tables += migrated_tables
total_rows += inserted_rows
logger.info("---")
logger.info(
f"SUMMARY: schemas={len(schemas)} "
f"tables_migrated={total_tables} "
f"rows_inserted={total_rows} "
f"errors={total_errors}"
)
# ----------------------
# CLI
# ----------------------
def parse_args(argv):
p = argparse.ArgumentParser(
description="Migrate Postgres data into per-schema SQLite DBs (non-invasive)."
)
p.add_argument("pg_conn", help="Postgres connection string")
p.add_argument(
"sqlite_dir", help="Directory containing per-schema sqlite .db files"
)
p.add_argument(
"--schema", help="Only migrate this schema (exact match)", default=None
)
p.add_argument(
"--dry-run",
help="Validate and report only; do not write to sqlite",
action="store_true",
)
p.add_argument("--verbose", help="Verbose logging (debug)", action="store_true")
p.add_argument("--no-color", help="Disable ANSI color output", action="store_true")
p.add_argument(
"--batch-size",
type=int,
default=DEFAULT_BATCH_SIZE,
help="Batch size for data fetching",
)
p.add_argument(
"--skip-tables",
help="Comma-separated list of tables to skip",
default="migrations,servers_stats",
)
return p.parse_args(argv[1:])
def main(argv):
args = parse_args(argv)
logger = setup_logger(args.verbose, args.no_color)
sqlite_dir_path = Path(args.sqlite_dir)
if not sqlite_dir_path.is_dir():
logger.error("SQLite directory does not exist or is not a directory.")
raise SystemExit(1)
try:
migrate_data(
args.pg_conn,
str(sqlite_dir_path),
args.schema,
args.dry_run,
args.skip_tables,
logger,
args.batch_size,
)
except Exception as e:
logger.error(f"Fatal error: {e}")
raise SystemExit(2)
if __name__ == "__main__":
main(sys.argv)
+2 -1
View File
@@ -10,7 +10,8 @@ LOAD DATABASE
-- pgloader implementation doesn't find "GENERATED ALWAYS AS IDENTITY" sequences,
-- instead we reset sequences manually via custom query after load
reset no sequences,
data only
data only,
workers = {{WORKERS}}
EXCLUDING TABLE NAMES LIKE 'migrations', 'sqlite_sequence'
+7 -2
View File
@@ -12,5 +12,10 @@ echo "$APPLE_SIMPLEX_SIGNING_KEYCHAIN" | base64 --decode -o /tmp/simplex.keychai
scripts/desktop/build-lib-mac.sh
cd apps/multiplatform
./gradlew packageDmg
./gradlew notarizeDmg
if [ -n "${ASSETS_DIR:-}" ]; then
set -- -Psimplex.assets.dir="$ASSETS_DIR"
else
set --
fi
./gradlew "$@" packageDmg
./gradlew "$@" notarizeDmg
+17 -9
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
set -e
@@ -7,7 +7,8 @@ function readlink() {
}
OS=linux
ARCH=${1:-`uname -a | rev | cut -d' ' -f2 | rev`}
ARCH="$(uname -m)"
DATABASE_BACKEND="${1:-sqlite}"
GHC_VERSION=8.10.7
if [ "$ARCH" == "aarch64" ]; then
@@ -24,18 +25,25 @@ exports=( $(sed 's/foreign export ccall "chat_migrate_init_key"//' src/Simplex/C
for elem in "${exports[@]}"; do count=$(grep -R "$elem$" libsimplex.dll.def | wc -l); if [ $count -ne 1 ]; then echo Wrong exports in libsimplex.dll.def. Add \"$elem\" to that file; exit 1; fi ; done
for elem in "${exports[@]}"; do count=$(grep -R "\"$elem\"" flake.nix | wc -l); if [ $count -ne 2 ]; then echo Wrong exports in flake.nix. Add \"$elem\" in two places of the file; exit 1; fi ; done
rm -rf $BUILD_DIR
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -flink-rts -threaded' --ghc-options="-optl-L$(ghc --print-libdir)/rts -optl-Wl,--as-needed,-lHSrts_thr-ghc$GHC_VERSION" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library'
#rm -rf $BUILD_DIR
if [[ "$DATABASE_BACKEND" == "postgres" ]]; then
echo "Building with postgres backend..."
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -optl-Wl,-soname,libsimplex.so -flink-rts -threaded' --constraint 'simplexmq +client_library +client_postgres' --constraint 'simplex-chat +client_library +client_postgres'
else
echo "Building with sqlite backend..."
cabal build lib:simplex-chat --ghc-options='-optl-Wl,-rpath,$ORIGIN -optl-Wl,-soname,libsimplex.so -flink-rts -threaded' --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library'
fi
cd $BUILD_DIR/build
#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
#patchelf --add-rpath '$ORIGIN' libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
mv libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so libsimplex.so 2> /dev/null || true
#patchelf --add-needed libHSrts_thr-ghc${GHC_VERSION}.so libsimplex.so
#patchelf --add-rpath '$ORIGIN' libsimplex.so
# GitHub's Ubuntu 20.04 runner started to set libffi.so.7 as a dependency while Ubuntu 20.04 on user's devices may not have it
# but libffi.so.8 is shipped as an external library with other libs
patchelf --replace-needed "libffi.so.7" "libffi.so.8" libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so
patchelf --replace-needed "libffi.so.7" "libffi.so.8" libsimplex.so
mkdir deps 2> /dev/null || true
ldd libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/
ldd libsimplex.so | grep "ghc" | cut -d' ' -f 3 | xargs -I {} cp {} ./deps/
cd -
@@ -44,7 +52,7 @@ rm -rf apps/multiplatform/desktop/build/cmake
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc${GHC_VERSION}.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp $BUILD_DIR/build/libsimplex.so apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
scripts/desktop/prepare-vlc-linux.sh
links_dir=apps/multiplatform/build/links
+6 -5
View File
@@ -3,7 +3,7 @@
set -e
OS=mac
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
ARCH="${1:-$(uname -m)}"
COMPOSE_ARCH=$ARCH
GHC_VERSION=9.6.3
DATABASE_BACKEND="${2:-sqlite}"
@@ -14,7 +14,7 @@ else
COMPOSE_ARCH=x64
fi
LIB_EXT=dylib
LIB=libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT
LIB=libsimplex.$LIB_EXT
GHC_LIBS_DIR=$(ghc --print-libdir)
BUILD_DIR=dist-newstyle/build/$ARCH-*/ghc-*/simplex-chat-*
@@ -27,13 +27,14 @@ rm -rf $BUILD_DIR
if [[ "$DATABASE_BACKEND" == "postgres" ]]; then
echo "Building with postgres backend..."
cabal build -f client_postgres lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library'
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-install_name,@rpath/$LIB -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library +client_postgres' --constraint 'simplex-chat +client_library +client_postgres'
else
echo "Building with sqlite backend..."
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library'
cabal build lib:simplex-chat lib:simplex-chat --ghc-options="-optl-Wl,-rpath,@loader_path -optl-Wl,-install_name,@rpath/$LIB -optl-Wl,-L$GHC_LIBS_DIR/$ARCH-osx-ghc-$GHC_VERSION -optl-lHSrts_thr-ghc$GHC_VERSION -optl-lffi" --constraint 'simplexmq +client_library' --constraint 'simplex-chat +client_library'
fi
cd $BUILD_DIR/build
mv libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT libsimplex.dylib 2> /dev/null || true
mkdir deps 2> /dev/null || true
# It's not included by default for some reason. Compiled lib tries to find system one but it's not always available
@@ -87,7 +88,7 @@ rm -rf apps/multiplatform/desktop/build/cmake
mkdir -p apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp -r $BUILD_DIR/build/deps/* apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp $BUILD_DIR/build/libHSsimplex-chat-*-inplace-ghc*.$LIB_EXT apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cp $BUILD_DIR/build/$LIB apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
cd apps/multiplatform/common/src/commonMain/cpp/desktop/libs/$OS-$ARCH/
+7 -2
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
set -e
@@ -18,7 +18,12 @@ libcrypto_path=$(ldd common/src/commonMain/cpp/desktop/libs/*/libHSdirect-sqlcip
trap "rm common/src/commonMain/cpp/desktop/libs/*/`basename $libcrypto_path` 2> /dev/null || true" EXIT
cp $libcrypto_path common/src/commonMain/cpp/desktop/libs/*
./gradlew createDistributable
if [ -n "${ASSETS_DIR:-}" ]; then
set -- -Psimplex.assets.dir="$ASSETS_DIR"
else
set --
fi
./gradlew "$@" createDistributable
rm common/src/commonMain/cpp/desktop/libs/*/`basename $libcrypto_path`
rm -rf $release_app_dir/AppDir 2>/dev/null
+10 -1
View File
@@ -4,7 +4,12 @@ ARCH="$(uname -m)"
scripts/desktop/build-lib-linux.sh
cd apps/multiplatform
./gradlew packageDeb
if [ -n "${ASSETS_DIR:-}" ]; then
set -- -Psimplex.assets.dir="$ASSETS_DIR"
else
set --
fi
./gradlew "$@" packageDeb
# Workaround for skiko library
#
@@ -37,8 +42,12 @@ export SOURCE_DATE_EPOCH=1704067200
dpkg-deb -R ./release/main/deb/simplex*.deb ./extracted
# Source the distribution variables (VERSION_CODENAME)
. /etc/os-release
rm -f ./extracted/opt/*imple*/lib/app/*skiko-awt-runtime-linux*
sed -i -e '/skiko-awt-runtime-linux/d' ./extracted/opt/*imple*/lib/app/simplex.cfg
sed -i "/Version/ s/\$/~$VERSION_CODENAME/" ./extracted/DEBIAN/control
find ./extracted/ -exec touch -d "@$SOURCE_DATE_EPOCH" {} +
dpkg-deb --build --root-owner-group --uniform-compression ./extracted ./release/main/deb/simplex_${ARCH}.deb
+1 -1
View File
@@ -1,4 +1,4 @@
#!/bin/bash
#!/usr/bin/env bash
set -e
+1 -1
View File
@@ -2,7 +2,7 @@
set -e
ARCH="${1:-`uname -a | rev | cut -d' ' -f1 | rev`}"
ARCH="${1:-$(uname -m)}"
if [ "$ARCH" == "arm64" ]; then
ARCH=aarch64
vlc_arch=arm64
@@ -38,6 +38,186 @@
</description>
<releases>
<release version="6.5.2" date="2026-05-15">
<url type="details">https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html</url>
<description>
<p>New in v6.5.2:</p>
<ul>
<li>allow deleting messages from channel history without time limit.</li>
</ul>
<p>New in v6.5:</p>
<p>Public channels - speak freely!</p>
<ul>
<li>Reliability: many relays per channel.</li>
<li>Ownership: you can run your own relays.</li>
<li>Security: owners hold channel keys.</li>
<li>Privacy: for owners and subscribers.</li>
</ul>
<p>Easier to invite your friends: we made connecting simpler for new users.</p>
<p>Safe web links:</p>
<ul>
<li>opt-in to send link previews.</li>
<li>use SOCKS proxy for previews (if enabled).</li>
<li>prevent hyperlink phishing.</li>
<li>remove link tracking.</li>
</ul>
<p>Non-profit governance: to make SimpleX Network last.</p>
</description>
</release>
<release version="6.5.1" date="2026-05-02">
<url type="details">https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html</url>
<description>
<p>New in v6.5.1:</p>
<ul>
<li>additional preset chat relay.</li>
<li>fixed a rare bug when receiving files.</li>
</ul>
<p>New in v6.5:</p>
<p>Public channels - speak freely!</p>
<ul>
<li>Reliability: many relays per channel.</li>
<li>Ownership: you can run your own relays.</li>
<li>Security: owners hold channel keys.</li>
<li>Privacy: for owners and subscribers.</li>
</ul>
<p>Easier to invite your friends: we made connecting simpler for new users.</p>
<p>Safe web links:</p>
<ul>
<li>opt-in to send link previews.</li>
<li>use SOCKS proxy for previews (if enabled).</li>
<li>prevent hyperlink phishing.</li>
<li>remove link tracking.</li>
</ul>
<p>Non-profit governance: to make SimpleX Network last.</p>
</description>
</release>
<release version="6.5.0" date="2026-04-30">
<url type="details">https://simplex.chat/blog/20260430-simplex-channels-v6-5-consortium-crowdfunding-freedom-of-speech.html</url>
<description>
<p>New in v6.5.</p>
<p>Public channels - speak freely!</p>
<ul>
<li>Reliability: many relays per channel.</li>
<li>Ownership: you can run your own relays.</li>
<li>Security: owners hold channel keys.</li>
<li>Privacy: for owners and subscribers.</li>
</ul>
<p>Easier to invite your friends: we made connecting simpler for new users.</p>
<p>Safe web links:</p>
<ul>
<li>opt-in to send link previews.</li>
<li>use SOCKS proxy for previews (if enabled).</li>
<li>prevent hyperlink phishing.</li>
<li>remove link tracking.</li>
</ul>
<p>Non-profit governance: to make SimpleX Network last.</p>
</description>
</release>
<release version="6.4.11" date="2026-03-30">
<url type="details">https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html</url>
<description>
<p>New in v6.4.11:</p>
<ul>
<li>improve image, video and link messages.</li>
</ul>
<p>New in v6.4-6.4.10:</p>
<ul>
<li>new UX to connect.</li>
<li>review new group members.</li>
<li>chat with group admins.</li>
<li>new UI languages: Catalan, Indonesian, Romanian and Vietnamese.</li>
<li>Linux app builds for aarch64 CPUs</li>
<li>UI support for bot commands.</li>
<li>support markdown hyperlinks, such as [click here](https://example.com).</li>
<li>option to remove tracking parameters from the links.</li>
<li>better information about network errors.</li>
</ul>
</description>
</release>
<release version="6.4.10" date="2026-01-29">
<url type="details">https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html</url>
<description>
<p>New in v6.4.10:</p>
<ul>
<li>improve error handling</li>
</ul>
<p>New in v6.4-6.4.8:</p>
<ul>
<li>new UX to connect.</li>
<li>review new group members.</li>
<li>chat with group admins.</li>
<li>new UI languages: Catalan, Indonesian, Romanian and Vietnamese.</li>
<li>Linux app builds for aarch64 CPUs</li>
<li>UI support for bot commands.</li>
<li>support markdown hyperlinks, such as [click here](https://example.com).</li>
<li>option to remove tracking parameters from the links.</li>
<li>better information about network errors.</li>
</ul>
</description>
</release>
<release version="6.4.8" date="2025-12-11">
<url type="details">https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html</url>
<description>
<p>New in v6.4.8:</p>
<ul>
<li>fix stuck message reception and other events after passphrase change (e.g., during desktop app initial start)</li>
</ul>
<p>New in v6.4-6.4.7:</p>
<ul>
<li>new UX to connect.</li>
<li>review new group members.</li>
<li>chat with group admins.</li>
<li>new UI languages: Catalan, Indonesian, Romanian and Vietnamese.</li>
<li>Linux app builds for aarch64 CPUs</li>
<li>UI support for bot commands.</li>
<li>support markdown hyperlinks, such as [click here](https://example.com).</li>
<li>option to remove tracking parameters from the links.</li>
<li>better information about network errors.</li>
</ul>
</description>
</release>
<release version="6.4.7" date="2025-11-03">
<url type="details">https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html</url>
<description>
<p>New in v6.4.7:</p>
<ul>
<li>fix exporting database larger than 4gb.</li>
</ul>
<p>New in v6.4-6.4.6:</p>
<ul>
<li>new UX to connect.</li>
<li>review new group members.</li>
<li>chat with group admins.</li>
<li>new UI languages: Catalan, Indonesian, Romanian and Vietnamese.</li>
<li>Linux app builds for aarch64 CPUs</li>
<li>UI support for bot commands.</li>
<li>support markdown hyperlinks, such as [click here](https://example.com).</li>
<li>option to remove tracking parameters from the links.</li>
<li>better information about network errors.</li>
</ul>
</description>
</release>
<release version="6.4.6" date="2025-10-05">
<url type="details">https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html</url>
<description>
<p>New in v6.4.6:</p>
<ul>
<li>fixed opening SimpleX links from outside of the app.</li>
</ul>
<p>New in v6.4-6.4.5:</p>
<ul>
<li>new UX to connect.</li>
<li>review new group members.</li>
<li>chat with group admins.</li>
<li>new UI languages: Catalan, Indonesian, Romanian and Vietnamese.</li>
<li>Linux app builds for aarch64 CPUs</li>
<li>UI support for bot commands.</li>
<li>support markdown hyperlinks, such as [click here](https://example.com).</li>
<li>option to remove tracking parameters from the links.</li>
<li>better information about network errors.</li>
</ul>
</description>
</release>
<release version="6.4.5" date="2025-09-08">
<url type="details">https://simplex.chat/blog/20250729-simplex-chat-v6-4-1-welcome-contacts-protect-groups-app-security.html</url>
<description>
+50
View File
@@ -0,0 +1,50 @@
#!/bin/sh
set -eu
# Copies generated iOS assets into SimpleXAssets.xcassets.
# Intended to run as an Xcode Run Script build phase.
# Skips silently if SIMPLEX_ASSETS is not in SWIFT_ACTIVE_COMPILATION_CONDITIONS
# or if the source directory is not found.
#
# The source path is resolved in order:
# 1. Command-line argument
# 2. SIMPLEX_ASSETS_DIR build setting (set in Local.xcconfig)
# 3. No default — skips if neither is set
#
# Manual usage: ./scripts/copy-assets.sh path/to/assets
# Skip if SIMPLEX_ASSETS flag is not set (unless run manually outside Xcode)
if [ -n "${SWIFT_ACTIVE_COMPILATION_CONDITIONS:-}" ]; then
case " $SWIFT_ACTIVE_COMPILATION_CONDITIONS " in
*" SIMPLEX_ASSETS "*) ;;
*) exit 0 ;;
esac
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
IOS_DIR="$SCRIPT_DIR/../../apps/ios/Shared/SimpleXAssets.xcassets"
ASSETS_ROOT="${1:-${SIMPLEX_ASSETS_DIR:-}}"
if [ -z "$ASSETS_ROOT" ]; then
echo "warning: SIMPLEX_ASSETS_DIR not set and no path argument provided" >&2
exit 0
fi
SRC_DIR="$ASSETS_ROOT/ios/Assets.xcassets"
if [ ! -d "$SRC_DIR" ]; then
echo "warning: source assets not found: $SRC_DIR (run resize.sh first)" >&2
exit 0
fi
# Remove old imagesets but keep root Contents.json
find "$IOS_DIR" -name "*.imageset" -type d -exec rm -rf {} + 2>/dev/null || true
# Copy imagesets
for imageset in "$SRC_DIR"/*.imageset; do
[ -d "$imageset" ] || continue
cp -r "$imageset" "$IOS_DIR/"
echo "Copied $(basename "$imageset")"
done
echo "Done. Assets copied to $IOS_DIR"
+1 -1
View File
@@ -1,5 +1,5 @@
{
"https://github.com/simplex-chat/simplexmq.git"."9346b85c3f34f8b12fefef4631ba21087cf5f0e3" = "010xj64rwj4g36f42znjv73c4armsdm6q9rdpxy1bqknwcbsk2sw";
"https://github.com/simplex-chat/simplexmq.git"."f03cec7a58ed13a39a52886888c74bcefdb64479" = "0bkd8kqgmwgfh5rwnw7s4p6mx9kwigi4jq9ljlfvzj23pslk1aq7";
"https://github.com/simplex-chat/hs-socks.git"."a30cc7a79a08d8108316094f8f2f82a0c5e1ac51" = "0yasvnr7g91k76mjkamvzab2kvlb1g5pspjyjn2fr6v83swjhj38";
"https://github.com/simplex-chat/direct-sqlcipher.git"."f814ee68b16a9447fbb467ccc8f29bdd3546bfd9" = "1ql13f4kfwkbaq7nygkxgw84213i0zm7c1a8hwvramayxl38dq5d";
"https://github.com/simplex-chat/sqlcipher-simple.git"."a46bd361a19376c5211f1058908fc0ae6bf42446" = "1z0r78d8f0812kxbgsm735qf6xx8lvaz27k1a0b4a2m0sshpd5gl";
+277
View File
@@ -0,0 +1,277 @@
#!/usr/bin/env sh
set -eu
SIMPLEX_KEY='3C:52:C4:FD:3C:AD:1C:07:C9:B0:0A:70:80:E3:58:FA:B9:FE:FC:B8:AF:5A:EC:14:77:65:F1:6D:0F:21:AD:85'
REPO_NAME="simplex-chat"
REPO="https://github.com/simplex-chat/${REPO_NAME}"
IMAGE_NAME='sx-local-android'
CONTAINER_NAME='sx-builder-android'
DOCKER_PATH_PROJECT='/project'
DOCKER_PATH_VERIFY='/verify'
export DOCKER_BUILDKIT=1
SIMPLEX_REPO='simplex-chat/simplex-chat'
CMDS="curl git docker"
INIT_DIR="$PWD"
TEMPDIR="$(mktemp -d)"
PID_MAX_ORIGINAL="$(sysctl -n kernel.pid_max)"
ARCHES="${ARCHES:-aarch64 armv7a}"
COLOR_CYAN="\033[36m"
COLOR_RESET="\033[0m"
SUFFIX_BUILT='built'
SUFFIX_DOWNLOADED='downloaded'
SUFFIX_BUILT_WITH_SIGNATURE='built-with-downloaded-signature'
cleanup() {
rm -rf -- "${TEMPDIR}"
docker rm --force "${CONTAINER_NAME}" 2>/dev/null || :
docker image rm "${IMAGE_NAME}" 2>/dev/null || :
if [ "$(sysctl -n kernel.pid_max)" != "$PID_MAX_ORIGINAL" ]; then
printf 'Adjusting kernel.pid_max back to original value...\n'
if $SUDO sysctl kernel.pid_max="$PID_MAX_ORIGINAL"; then
printf 'Successfully adjusted kernel.pid_max\n'
else
printf 'Failed to adjust kernel.pid_max. Please set the value manually with: %s sysctl kernel.pid_max=%s\n' "$SUDO" "$PID_MAX_ORIGINAL"
fi
fi
}
trap 'cleanup' EXIT INT
check() {
commands="$1"
set +u
for i in $commands; do
if ! command -v "$i" > /dev/null 2>&1; then
commands_failed="$i $commands_failed"
fi
done
if [ -n "$commands_failed" ]; then
commands_failed=${commands_failed% *}
printf "%s is not found in your \$PATH. Please install them and re-run the script.\n" "$commands_failed"
exit 1
fi
if [ "$PID_MAX_ORIGINAL" -gt 65535 ]; then
SUDO=$(command -v sudo || command -v doas) || { echo "No sudo or doas"; exit 1; }
printf 'Adjusting kernel.pid_max value to 65535...\n'
if $SUDO sysctl kernel.pid_max=65535; then
printf 'Successfully adjusted kernel.pid_max\n'
else
printf 'Failed to adjust kernel.pid_max, aborting.\n'
exit 1
fi
fi
set -u
}
download_apk() {
tag="$1"
filename="$2"
file_out="$3"
curl -L "${REPO}/releases/download/${tag}/${filename}" -o "$file_out"
}
setup_git() {
workdir="$1"
name="$2"
git -C "$workdir" clone "${REPO}.git" "$name"
}
checkout_git() {
git_dir="$1"
tag="$2"
git -C "$git_dir" reset --hard
git -C "$git_dir" clean -dfx
git -C "$git_dir" checkout "$tag"
}
check_apk() {
apk_name="$1"
expected="$2"
actual=$(docker exec "${CONTAINER_NAME}" apksigner verify --print-certs "${DOCKER_PATH_VERIFY}/${apk_name}" | grep 'SHA-256' | awk '{print $NF}' | fold -w2 | paste -sd: | tr '[:lower:]' '[:upper:]')
if [ "$expected" = "$actual" ]; then
return 0
else
return 1
fi
}
verify_apk() {
apk_name="$1"
# https://github.com/obfusk/apksigcopier?tab=readme-ov-file#what-about-signatures-made-by-apksigner-from-build-tools--3500-rc1
docker exec "${CONTAINER_NAME}" repro-apk zipalign --page-size 16 --pad-like-apksigner --replace "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT}" \
"${DOCKER_PATH_VERIFY}/${apk_name}.aligned"
docker exec "${CONTAINER_NAME}" mv "${DOCKER_PATH_VERIFY}/${apk_name}.aligned" \
"${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT}"
docker exec "${CONTAINER_NAME}" apksigcopier copy "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_DOWNLOADED}" \
"${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT}" \
"${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT_WITH_SIGNATURE}"
downloaded_apk_hash=$(docker exec "${CONTAINER_NAME}" sha256sum "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_DOWNLOADED}" | awk '{print $1}')
built_apk_hash=$(docker exec "${CONTAINER_NAME}" sha256sum "${DOCKER_PATH_VERIFY}/${apk_name}.${SUFFIX_BUILT_WITH_SIGNATURE}" | awk '{print $1}')
if [ "$downloaded_apk_hash" = "$built_apk_hash" ]; then
return 0
else
return 1
fi
}
print_vercode() {
build_dir="$1"
awk -F'=' '/android.version_code=/ {print $2}' "${build_dir}/apps/multiplatform/gradle.properties"
}
setup_container() {
dir_git="$1"
dir_apk="$2"
docker build \
--no-cache \
-f "${dir_git}/Dockerfile.build" \
-t "${IMAGE_NAME}" \
--build-arg=USER_UID="$(id -u)" \
--build-arg=USER_GID="$(id -g)" \
.
# Run container in background
docker run -t -d \
--name "${CONTAINER_NAME}" \
--device /dev/fuse \
--cap-add SYS_ADMIN \
--security-opt apparmor:unconfined \
--security-opt seccomp:unconfined \
-v "${dir_git}:${DOCKER_PATH_PROJECT}" \
-v "${dir_apk}:${DOCKER_PATH_VERIFY}" \
"${IMAGE_NAME}"
}
build_apk() {
arch="$1"
vercode="$2"
apk_out="simplex-${arch}.apk.${SUFFIX_BUILT}"
# Gradle setup
docker exec -i "${CONTAINER_NAME}" sh << EOF
cd $DOCKER_PATH_PROJECT/apps/multiplatform
./gradlew
EOF
docker exec -i "${CONTAINER_NAME}" sh << EOF
GRADLE_BIN=\$(find \$HOME/.gradle/wrapper/dists -name "gradle" -type f -executable 2>/dev/null | head -1)
GRADLE_DIR=\$(dirname "\$GRADLE_BIN")
export PATH="\$GRADLE_DIR:\$PATH"
ARCHES="$arch" ./scripts/android/build-android.sh -gs "$vercode" || ARCHES="$arch" ./scripts/android/build-android.sh -gs "$vercode"
APK_FILE=\$(find . -maxdepth 1 -type f -name '*.apk')
mv "\$APK_FILE" $DOCKER_PATH_VERIFY/$apk_out
EOF
}
main() {
tag="$1"
build_directory="${TEMPDIR}/${REPO_NAME}"
final_directory="$INIT_DIR/${tag}-${REPO_NAME}"
apk_directory="${final_directory}/android"
printf 'This script will:
1) build docker container.
2) download APK from GitHub and validate signatures.
3) build core library with nix (12-24 hours).
4) build APK and compare with downloaded one
The script will ask for sudo password to adjust kernel.pid_max (needed for armv7a build)
and set it back to otiginal value when the build is done.
Continue?'
read _
check "$CMDS"
mkdir -p "${apk_directory}"
# Setup initial git for Dockerfile.build
setup_git "$TEMPDIR" "$REPO_NAME"
checkout_git "$build_directory" "$tag"
printf "${COLOR_CYAN}Building Docker container...${COLOR_RESET}\n"
setup_container "$build_directory" "$apk_directory"
# Check phase
for arch in $ARCHES; do
filename="simplex-${arch}.apk"
download_apk "$tag" "$filename" "${apk_directory}/${filename}.${SUFFIX_DOWNLOADED}"
if check_apk "${filename}.${SUFFIX_DOWNLOADED}" "$SIMPLEX_KEY"; then
printf "${COLOR_CYAN}APK for %s is signed by valid key.${COLOR_RESET}\n" "$arch"
else
printf "${COLOR_CYAN}Signature of APK for %s is invalid., aborting the script.${COLOR_RESET}\n" "$arch"
exit 1
fi
done
# Build phase
for arch in $ARCHES; do
case "$arch" in
armv7a)
build_tag="${tag}-armv7a"
;;
aarch64)
build_tag="${tag}"
;;
*)
printf "${COLOR_CYAN}Unknown architecture: %s! Skipping the build...${COLOR_RESET}\n" "$arch"
continue
esac
# Setup the code
checkout_git "$build_directory" "$build_tag"
vercode=$(print_vercode "$build_directory")
printf "${COLOR_CYAN}Building APK for for %s...${COLOR_RESET}\n" "$arch"
build_apk "$arch" "$vercode"
done
# Verification phase
for arch in $ARCHES; do
filename="simplex-${arch}.apk"
if ! verify_apk "$filename"; then
printf "${COLOR_CYAN}Failed to verify %s! Aborting.\n${COLOR_RESET}" "$filename"
exit 1
fi
done
printf "${COLOR_CYAN}%s is reproducible.${COLOR_RESET}\n" "$tag"
cleanup
}
main "$@"
+10 -3
View File
@@ -38,7 +38,11 @@ git -C "${tempdir}" clone "${repo}.git" &&\
cd "${tempdir}/${repo_name}" &&\
git checkout "${TAG}"
for os in '22.04' '24.04'; do
oses="22.04@sha256:5c8b2c0a6c745bc177669abfaa716b4bc57d58e2ea3882fb5da67f4d59e3dda5 24.04@sha256:98ff7968124952e719a8a69bb3cccdd217f5fe758108ac4f21ad22e1df44d237"
for os_pair in ${oses}; do
os="${os_pair%@*}"
hash="${os_pair#*@}"
os_url="$(printf '%s' "${os}" | tr '.' '_')"
cli_name="simplex-chat-ubuntu-${os_url}-x86_64"
@@ -49,7 +53,10 @@ for os in '22.04' '24.04'; do
docker build \
--no-cache \
--build-arg TAG="${os}" \
--build-arg HASH="${hash}" \
--build-arg GHC="${ghc}" \
--build-arg=USER_UID="$(id -u)" \
--build-arg=USER_GID="$(id -g)" \
-f "${tempdir}/${repo_name}/Dockerfile.build" \
-t "${image_name}" \
.
@@ -103,7 +110,7 @@ for os in '22.04' '24.04'; do
# Desktop: deb
docker exec \
-t "${container_name}" \
sh -c './scripts/desktop/make-deb-linux.sh'
sh -c "export ASSETS_DIR='../../assets'; ./scripts/desktop/make-deb-linux.sh"
# Copy deb
docker cp \
@@ -121,7 +128,7 @@ for os in '22.04' '24.04'; do
# Appimage
docker exec \
-t "${container_name}" \
sh -c './scripts/desktop/make-appimage-linux.sh && mv ./apps/multiplatform/release/main/*imple*.AppImage ./apps/multiplatform/release/main/simplex.appimage'
sh -c "export ASSETS_DIR='../../assets'; ./scripts/desktop/make-appimage-linux.sh && mv ./apps/multiplatform/release/main/*imple*.AppImage ./apps/multiplatform/release/main/simplex.appimage"
# Copy appimage
docker cp \