diff --git a/.changeset/polyfill-change-array-by-copy.md b/.changeset/polyfill-change-array-by-copy.md new file mode 100644 index 0000000000..7ba827c45a --- /dev/null +++ b/.changeset/polyfill-change-array-by-copy.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Fix a crash on older WebViews that lack `toSorted`, `toReversed`, and `with`. diff --git a/.changeset/shrink-foss-android-apk.md b/.changeset/shrink-foss-android-apk.md new file mode 100644 index 0000000000..f8d8a5b24b --- /dev/null +++ b/.changeset/shrink-foss-android-apk.md @@ -0,0 +1,5 @@ +--- +default: patch +--- + +Shrink the Android APK: strip the native library, drop bundled source maps, and split the F-Droid build per ABI. diff --git a/.github/workflows/tauri-build.yml b/.github/workflows/tauri-build.yml index adf3a41b7a..1223c22409 100644 --- a/.github/workflows/tauri-build.yml +++ b/.github/workflows/tauri-build.yml @@ -326,7 +326,7 @@ jobs: echo "storeFile=$RUNNER_TEMP/keystore.jks" >> keystore.properties # Must run before the Google build: they share one Gradle output directory. - - name: Build FOSS Android APK + - name: Build FOSS Android APKs if: ${{ env.IS_RELEASE == 'true' }} shell: bash env: @@ -335,13 +335,16 @@ jobs: # tauri-build validates every file in capabilities/, so the geolocation # permissions must be absent, not just unreferenced. rm src-tauri/capabilities/geolocation.json - pnpm tauri android build --apk --target aarch64 armv7 -- --no-default-features --features wry,matrix-crypto + pnpm tauri android build --apk --split-per-abi --target aarch64 armv7 -- --no-default-features --features wry,matrix-crypto git checkout -- src-tauri/capabilities/geolocation.json - OUT='src-tauri/gen/android/app/build/outputs/apk/universal/release' - APK=$(find "$OUT" -name '*.apk' -type f | head -1) - [ -n "$APK" ] || { echo 'FOSS APK not found' >&2; exit 1; } mkdir -p android-artifacts - mv "$APK" "android-artifacts/Sable-${VERSION}-android-universal-foss.apk" + for pair in 'arm64:arm64' 'arm:armv7'; do + FLAVOR=${pair%%:*} + ABI=${pair##*:} + APK=$(find "src-tauri/gen/android/app/build/outputs/apk/$FLAVOR/release" -name '*.apk' -type f | head -1) + [ -n "$APK" ] || { echo "FOSS $ABI APK not found" >&2; exit 1; } + mv "$APK" "android-artifacts/Sable-${VERSION}-android-${ABI}-foss.apk" + done - name: Build Android bundles shell: bash diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 71ad20ad3e..48dbff9b44 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -243,3 +243,8 @@ tauri-typegen = { git = "https://github.com/SableClient/tauri-typegen", branch = # tauri-plugin-store = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } # tauri-plugin-single-instance = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } # tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "feat/cef" } + +[profile.release] +strip = "symbols" +lto = "thin" +codegen-units = 1 diff --git a/src-tauri/gen/android/app/build.gradle.kts b/src-tauri/gen/android/app/build.gradle.kts index c65309122a..d40f98bae6 100644 --- a/src-tauri/gen/android/app/build.gradle.kts +++ b/src-tauri/gen/android/app/build.gradle.kts @@ -58,6 +58,9 @@ android { getByName("release") { signingConfig = signingConfigs.getByName("release") isMinifyEnabled = true + packaging { + jniLibs.useLegacyPackaging = true + } proguardFiles( *fileTree(".") { include("**/*.pro") } .plus(getDefaultProguardFile("proguard-android-optimize.txt")) diff --git a/src/arrayCompat.test.ts b/src/arrayCompat.test.ts new file mode 100644 index 0000000000..cf1c6deb6c --- /dev/null +++ b/src/arrayCompat.test.ts @@ -0,0 +1,45 @@ +import { afterEach, describe, expect, it } from 'vitest'; +import { installChangeArrayByCopyPolyfill } from './arrayCompat'; + +const proto = Array.prototype as { + toSorted?: unknown; + toReversed?: unknown; + with?: unknown; +}; +const originals = { + toSorted: proto.toSorted, + toReversed: proto.toReversed, + with: proto.with, +}; + +afterEach(() => { + proto.toSorted = originals.toSorted; + proto.toReversed = originals.toReversed; + proto.with = originals.with; +}); + +describe('installChangeArrayByCopyPolyfill', () => { + it('installs the methods when the runtime does not provide them', () => { + Reflect.deleteProperty(proto, 'toSorted'); + Reflect.deleteProperty(proto, 'toReversed'); + Reflect.deleteProperty(proto, 'with'); + + installChangeArrayByCopyPolyfill(); + + const source = [3, 1, 2]; + expect(source.toSorted((a, b) => a - b)).toEqual([1, 2, 3]); + expect(source.toReversed()).toEqual([2, 1, 3]); + // oxlint-disable-next-line unicorn/no-confusing-array-with + expect(source.with(-1, 9)).toEqual([3, 1, 9]); + expect(source).toEqual([3, 1, 2]); + expect(() => source.with(3, 9)).toThrow(RangeError); + }); + + it('installs them as non-enumerable', () => { + Reflect.deleteProperty(proto, 'toSorted'); + + installChangeArrayByCopyPolyfill(); + + expect(Object.keys([1, 2])).toEqual(['0', '1']); + }); +}); diff --git a/src/arrayCompat.ts b/src/arrayCompat.ts new file mode 100644 index 0000000000..21c1bf07c3 --- /dev/null +++ b/src/arrayCompat.ts @@ -0,0 +1,40 @@ +type ChangeArrayByCopyMethods = { + toSorted?: unknown; + toReversed?: unknown; + with?: unknown; +}; + +function define(name: string, value: unknown): void { + // oxlint-disable-next-line no-extend-native + Object.defineProperty(Array.prototype, name, { value, writable: true, configurable: true }); +} + +export function installChangeArrayByCopyPolyfill(): void { + const proto = Array.prototype as ChangeArrayByCopyMethods; + + if (!proto.toSorted) { + define('toSorted', function toSorted(this: T[], compare?: (a: T, b: T) => number): T[] { + // oxlint-disable-next-line unicorn/no-array-sort + return Array.prototype.slice.call(this).sort(compare); + }); + } + + if (!proto.toReversed) { + define('toReversed', function toReversed(this: T[]): T[] { + // oxlint-disable-next-line unicorn/no-array-reverse + return Array.prototype.slice.call(this).reverse(); + }); + } + + if (!proto.with) { + define('with', function withAt(this: T[], index: number, value: T): T[] { + const copy = Array.prototype.slice.call(this) as T[]; + const target = index < 0 ? copy.length + index : index; + if (target < 0 || target >= copy.length) throw new RangeError(`Invalid index : ${index}`); + copy[target] = value; + return copy; + }); + } +} + +installChangeArrayByCopyPolyfill(); diff --git a/src/instrument.ts b/src/instrument.ts index 5539aee377..17af86b1e1 100644 --- a/src/instrument.ts +++ b/src/instrument.ts @@ -7,6 +7,7 @@ * - VITE_APP_VERSION: Release version for tracking */ /* oxlint-disable no-console */ +import './arrayCompat'; import './promiseCompat'; import * as Sentry from '@sentry/react'; import React from 'react'; diff --git a/vite.config.ts b/vite.config.ts index aa91ce9aa5..6dfb3fb83b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -80,7 +80,7 @@ const callEmbeddedDir = 'node_modules/@sableclient/sable-call-embedded/dist'; const copyFiles = { targets: [ { - src: callEmbeddedDir, + src: [callEmbeddedDir, `!${callEmbeddedDir}/**/*.map`], dest: 'public/element-call', rename: { stripBase: callEmbeddedDir.split('/').length }, },