mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
chore: update native packaging
This commit is contained in:
1 parent
d0abbaaa4f
commit
4a32a2c4f2
22 files changed
+466
-7
No files matched your search
@@ -2,6 +2,10 @@
|
||||
# one disposable build tree. Cargo resolves this relative path to <repo>/target.
|
||||
[build]
|
||||
target-dir = "target"
|
||||
# Compiling Slint's generated library and test harness together can exhaust
|
||||
# memory on a 16 GiB development host, including editor background checks.
|
||||
# Larger builders can opt into more parallelism with Cargo's -j option.
|
||||
jobs = 1
|
||||
|
||||
# Slint's desktop guide recommends matching Linux's larger main-thread stack on
|
||||
# Windows. This prevents stack overflows in debug builds with deep UI trees.
|
||||
@@ -10,3 +14,11 @@ rustflags = ["-C", "link-arg=/STACK:8000000"]
|
||||
|
||||
[target.aarch64-pc-windows-msvc]
|
||||
rustflags = ["-C", "link-arg=/STACK:8000000"]
|
||||
|
||||
# NDK r27 and newer devices must agree on 16 KiB ELF load alignment. This also
|
||||
# remains compatible with 4 KiB Android devices and matches bundled PDFium.
|
||||
[target.aarch64-linux-android]
|
||||
rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
|
||||
|
||||
[target.x86_64-linux-android]
|
||||
rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]
|
||||
@@ -210,6 +210,8 @@ jobs:
|
||||
New-Item -ItemType Directory -Force target/package/windows | Out-Null
|
||||
New-Item -ItemType Directory -Force "target/package/windows/Licenses/Google Sans Flex" | Out-Null
|
||||
New-Item -ItemType Directory -Force "target/package/windows/Licenses/Noto Emoji" | Out-Null
|
||||
python scripts/stage-pdfium.py win-x64 target/package/windows
|
||||
if ($LASTEXITCODE -ne 0) { throw "PDFium staging failed" }
|
||||
Copy-Item target/release/flectar-mail.exe target/package/windows/
|
||||
Copy-Item README.md target/package/windows/
|
||||
Copy-Item LICENSE target/package/windows/
|
||||
@@ -219,6 +221,8 @@ jobs:
|
||||
Copy-Item resources/fonts/google-sans-flex/README.md "target/package/windows/Licenses/Google Sans Flex/"
|
||||
Copy-Item resources/fonts/noto-emoji/OFL.txt "target/package/windows/Licenses/Noto Emoji/"
|
||||
Copy-Item resources/fonts/noto-emoji/README.md "target/package/windows/Licenses/Noto Emoji/"
|
||||
python scripts/test-pdf-preview.py target/package/windows/flectar-mail.exe
|
||||
if ($LASTEXITCODE -ne 0) { throw "Packaged PDF preview test failed" }
|
||||
Compress-Archive -Path target/package/windows/* -DestinationPath target/flectar-mail-windows-x64.zip -Force
|
||||
|
||||
- name: Install pinned Inno Setup compiler
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
*.AppImage
|
||||
/platform/android/gradle/.gradle/
|
||||
/platform/android/gradle/app/build/
|
||||
/platform/android/gradle/build/
|
||||
/platform/ios/*.xcodeproj/
|
||||
|
||||
# Local OAuth application registrations (see .env.example)
|
||||
|
||||
@@ -36,6 +36,23 @@ notices and license texts; the Apache-2.0 license is also included in
|
||||
| percent-encoding | 2.3.2 | [servo/rust-url](https://github.com/servo/rust-url/) | Fragment and mailto decoding |
|
||||
| base64 | 0.22.1 | [marshallpierce/rust-base64](https://github.com/marshallpierce/rust-base64) | Export CSP hash encoding |
|
||||
|
||||
## PDF preview
|
||||
|
||||
Desktop, Android, and iOS packages include a non-V8 PDFium runtime from
|
||||
[bblanchon/pdfium-binaries](https://github.com/bblanchon/pdfium-binaries), built
|
||||
from [PDFium](https://pdfium.googlesource.com/pdfium/). The selected release and
|
||||
per-platform SHA-256 digests are pinned in `scripts/stage-pdfium.py`. Every
|
||||
package retains the archive's `LICENSE`, complete `licenses/` directory, and
|
||||
build provenance under `pdfium-licenses` beside the private desktop library,
|
||||
Android assets at `licenses/PDFium`, and iOS bundle resources at `Licenses/PDFium`.
|
||||
PDFium's third-party components retain their respective licenses.
|
||||
|
||||
The Rust interface is [pdfium-render](https://github.com/ajrcarey/pdfium-render)
|
||||
0.9.4, used under its Apache-2.0 option. Linux process restrictions use
|
||||
[Landlock](https://github.com/landlock-lsm/rust-landlock) and
|
||||
[seccompiler](https://github.com/rust-vmm/seccompiler), under their Apache-2.0
|
||||
options. Rust package versions and source checksums are recorded in `Cargo.lock`.
|
||||
|
||||
## Phosphor Icons
|
||||
|
||||
The bundled SVG icons in `ui/icons/phosphor` are sourced from
|
||||
|
||||
@@ -26,6 +26,10 @@ android {
|
||||
layout.buildDirectory.dir("generated/flectar-assets"),
|
||||
)
|
||||
|
||||
// PDFium is loaded by its absolute installed nativeLibraryDir path. Let the
|
||||
// package manager extract and protect it; no temporary executable files.
|
||||
packaging.jniLibs.useLegacyPackaging = true
|
||||
|
||||
signingConfigs {
|
||||
create("releaseFromEnvironment") {
|
||||
val keystore = providers.environmentVariable("FLECTAR_ANDROID_KEYSTORE").orNull
|
||||
@@ -56,8 +60,28 @@ val generateFlectarLicenseAssets by tasks.registering(Copy::class) {
|
||||
}
|
||||
}
|
||||
|
||||
val generatePdfiumLicenseAssets by tasks.registering(Copy::class) {
|
||||
into(layout.buildDirectory.dir("generated/flectar-assets/licenses/PDFium"))
|
||||
from(repositoryAndroidTarget.dir("gradle-jni")) {
|
||||
include("*/pdfium-licenses/**")
|
||||
}
|
||||
}
|
||||
val verifyPdfiumRuntime by tasks.registering {
|
||||
doLast {
|
||||
fileTree(repositoryAndroidTarget.dir("gradle-jni")).matching {
|
||||
include("*/libflectar_mail_android.so")
|
||||
}.forEach { appLibrary ->
|
||||
check(appLibrary.resolveSibling("libpdfium.so").isFile) {
|
||||
"Missing bundled PDFium for ${appLibrary.parentFile.name}; run scripts/stage-pdfium.py for this ABI."
|
||||
}
|
||||
check(appLibrary.parentFile.resolve("pdfium-licenses/build.json").isFile) {
|
||||
"Missing PDFium provenance and license notices."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tasks.named("preBuild").configure {
|
||||
dependsOn(generateFlectarLicenseAssets)
|
||||
dependsOn(generateFlectarLicenseAssets, generatePdfiumLicenseAssets, verifyPdfiumRuntime)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -15,11 +15,12 @@ export CARGO_PROFILE_RELEASE_DEBUG="${CARGO_PROFILE_RELEASE_DEBUG:-1}"
|
||||
export CARGO_TARGET_DIR="${DERIVED_FILE_DIR:?}/cargo"
|
||||
|
||||
is_simulator=0
|
||||
if [[ "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then
|
||||
if [[ "${PLATFORM_NAME:-}" == "iphonesimulator" || "${LLVM_TARGET_TRIPLE_SUFFIX:-}" == "-simulator" ]]; then
|
||||
is_simulator=1
|
||||
fi
|
||||
|
||||
executables=()
|
||||
pdfium_libraries=()
|
||||
for arch in $ARCHS; do
|
||||
case "$arch" in
|
||||
arm64)
|
||||
@@ -47,10 +48,48 @@ for arch in $ARCHS; do
|
||||
--features remote-content,ios \
|
||||
"${release_args[@]}"
|
||||
executables+=("$CARGO_TARGET_DIR/$cargo_target/$profile/$binary_name")
|
||||
case "$cargo_target" in
|
||||
aarch64-apple-ios) pdfium_target=ios-device-arm64 ;;
|
||||
aarch64-apple-ios-sim) pdfium_target=ios-simulator-arm64 ;;
|
||||
x86_64-apple-ios) pdfium_target=ios-simulator-x64 ;;
|
||||
esac
|
||||
pdfium_stage="$DERIVED_FILE_DIR/pdfium/$pdfium_target"
|
||||
python3 "$project_dir/scripts/stage-pdfium.py" "$pdfium_target" "$pdfium_stage" \
|
||||
--cache "$project_dir/target/pdfium-downloads"
|
||||
pdfium_libraries+=("$pdfium_stage/libpdfium.dylib")
|
||||
pdfium_notices="$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/Licenses/PDFium/$pdfium_target"
|
||||
mkdir -p "$pdfium_notices"
|
||||
cp -R "$pdfium_stage/pdfium-licenses/." "$pdfium_notices/"
|
||||
done
|
||||
|
||||
lipo -create -output "$TARGET_BUILD_DIR/$EXECUTABLE_PATH" "${executables[@]}"
|
||||
|
||||
# iOS permits embedded dynamic frameworks, not loose third-party dylibs.
|
||||
# Device and simulator slices are never mixed; Xcode chooses the SDK above.
|
||||
framework="$TARGET_BUILD_DIR/${FRAMEWORKS_FOLDER_PATH:?}/PDFium.framework"
|
||||
mkdir -p "$framework"
|
||||
lipo -create -output "$framework/PDFium" "${pdfium_libraries[@]}"
|
||||
chmod 755 "$framework/PDFium"
|
||||
install_name_tool -id '@rpath/PDFium.framework/PDFium' "$framework/PDFium"
|
||||
python3 "$project_dir/scripts/write-pdfium-framework-plist.py" "$framework/Info.plist" "$is_simulator"
|
||||
if [[ "${CODE_SIGNING_ALLOWED:-YES}" != "NO" ]]; then
|
||||
# Simulator frameworks also need an ad-hoc signature after install_name_tool.
|
||||
if [[ "$is_simulator" -eq 1 ]]; then
|
||||
framework_identity="${EXPANDED_CODE_SIGN_IDENTITY:--}"
|
||||
else
|
||||
framework_identity="${EXPANDED_CODE_SIGN_IDENTITY:?A device build requires an Xcode signing identity}"
|
||||
fi
|
||||
codesign --force --sign "$framework_identity" "$framework"
|
||||
codesign --verify --strict "$framework"
|
||||
fi
|
||||
# Fail packaging if an unexpected dependency or install name escaped staging.
|
||||
framework_dependencies="$(otool -L "$framework/PDFium")"
|
||||
if printf '%s\n' "$framework_dependencies" | awk '/^[[:space:]]/ {print $1}' \
|
||||
| grep -Ev '^(@rpath/PDFium.framework/PDFium|/System/Library/|/usr/lib/)'; then
|
||||
printf 'PDFium framework has an unbundled dependency.\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The UI fonts are embedded in the executable. Ship their OFL notices in the
|
||||
# application bundle as required for redistributed copies of the fonts.
|
||||
font_license_root="$TARGET_BUILD_DIR/${UNLOCALIZED_RESOURCES_FOLDER_PATH:?}/Licenses"
|
||||
|
||||
@@ -10,7 +10,7 @@ targets:
|
||||
FlectarMail:
|
||||
type: application
|
||||
platform: iOS
|
||||
deploymentTarget: "12.0"
|
||||
deploymentTarget: "17.0"
|
||||
info:
|
||||
path: Info.plist
|
||||
properties:
|
||||
|
||||
@@ -85,6 +85,10 @@ fi
|
||||
export MALLOC_ARENA_MAX="${MALLOC_ARENA_MAX:-1}"
|
||||
export MALLOC_TRIM_THRESHOLD_="${MALLOC_TRIM_THRESHOLD_:-65536}"
|
||||
|
||||
# Preserve the host search path for external programs such as the browser.
|
||||
export FLECTAR_MAIL_HOST_LD_LIBRARY_PATH="${LD_LIBRARY_PATH-}"
|
||||
export FLECTAR_MAIL_BUNDLED=1
|
||||
|
||||
# linuxdeploy places any non-system ELF dependencies here.
|
||||
export LD_LIBRARY_PATH="$app_dir/usr/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
# The Linux binary supports Slint's Wayland and X11 winit integrations. Slint
|
||||
|
||||
@@ -5,7 +5,7 @@ Priority: optional
|
||||
Architecture: amd64
|
||||
Maintainer: Flectar <flectar@users.noreply.github.com>
|
||||
Depends: @DEPENDS@
|
||||
Recommends: xdg-desktop-portal
|
||||
Recommends: xdg-desktop-portal, gnupg, pinentry-gnome3 | pinentry-qt
|
||||
Homepage: https://flectar.com
|
||||
Description: Native Slint mail client
|
||||
Flectar Mail is a native desktop mail client built with Rust and Slint.
|
||||
@@ -8,7 +8,7 @@ Homepage: https://flectar.com
|
||||
|
||||
Package: flectar-mail
|
||||
Architecture: amd64
|
||||
Depends: ${shlibs:Depends}, libfontconfig1, libwayland-client0, libx11-6, libx11-xcb1, libxkbcommon0, libxkbcommon-x11-0, hicolor-icon-theme
|
||||
Depends: ${shlibs:Depends}, libfontconfig1, libwayland-client0, libx11-6, libx11-xcb1, libxkbcommon0, libxkbcommon-x11-0, hicolor-icon-theme, xdg-utils
|
||||
Recommends: xdg-desktop-portal
|
||||
Description: Native Slint mail client
|
||||
Flectar Mail is a native desktop mail client built with Rust and Slint.
|
||||
@@ -91,6 +91,8 @@ native_lib="$target_dir/aarch64-linux-android/release/libflectar_mail_android.so
|
||||
jni_dir="$target_dir/android/gradle-jni/arm64-v8a"
|
||||
mkdir -p "$jni_dir"
|
||||
cp "$native_lib" "$jni_dir/libflectar_mail_android.so"
|
||||
python3 "$project_dir/scripts/stage-pdfium.py" android-arm64 "$jni_dir" \
|
||||
--cache "$target_dir/pdfium-downloads"
|
||||
|
||||
"$gradle_command" --no-daemon \
|
||||
--project-cache-dir "$target_dir/android/gradle-cache" \
|
||||
|
||||
@@ -87,6 +87,8 @@ native_lib="$target_dir/aarch64-linux-android/release/libflectar_mail_android.so
|
||||
jni_dir="$target_dir/android/gradle-jni/arm64-v8a"
|
||||
mkdir -p "$jni_dir"
|
||||
cp "$native_lib" "$jni_dir/libflectar_mail_android.so"
|
||||
python3 "$project_dir/scripts/stage-pdfium.py" android-arm64 "$jni_dir" \
|
||||
--cache "$target_dir/pdfium-downloads"
|
||||
|
||||
export FLECTAR_ANDROID_KEYSTORE="$keystore"
|
||||
export FLECTAR_ANDROID_KEYSTORE_PASSWORD=android
|
||||
|
||||
@@ -54,6 +54,7 @@ mkdir -p \
|
||||
"$app_dir/usr/share/icons/hicolor/scalable/apps" \
|
||||
"$app_dir/usr/share/metainfo"
|
||||
cp "$project_dir/target/release/flectar-mail" "$app_dir/usr/bin/flectar-mail"
|
||||
python3 "$project_dir/scripts/stage-pdfium.py" linux-x64 "$app_dir/usr/lib/flectar-mail"
|
||||
app_version="$(python3 "$project_dir/scripts/stage-linux-metadata.py" "$app_dir")"
|
||||
cp "$project_dir/resources/app-icon/flectar-mail-masked-512.png" "$app_dir/usr/share/icons/hicolor/512x512/apps/com.flectar.mail.png"
|
||||
cp "$project_dir/resources/app-icon/flectar-mail-masked.svg" "$app_dir/usr/share/icons/hicolor/scalable/apps/com.flectar.mail.svg"
|
||||
@@ -87,6 +88,10 @@ VERSION="$app_version" APPIMAGE_EXTRACT_AND_RUN="${APPIMAGE_EXTRACT_AND_RUN:-1}"
|
||||
cp "$project_dir/resources/AppRun" "$app_dir/AppRun"
|
||||
chmod +x "$app_dir/AppRun"
|
||||
|
||||
# The pinned PDFium runtime depends only on the ordinary glibc runtime. Keep
|
||||
# its private path intact and verify it after deployment has adjusted the ELF files.
|
||||
python3 "$project_dir/scripts/test-pdf-preview.py" "$app_dir/usr/bin/flectar-mail"
|
||||
|
||||
# AppDir specifies a PNG .DirIcon for file-manager thumbnails. linuxdeploy
|
||||
# prefers the scalable icon for the root entry, so restore the PNG thumbnail
|
||||
# explicitly while retaining the SVG and themed icon copies for integration.
|
||||
|
||||
@@ -33,6 +33,8 @@ mkdir -p \
|
||||
|
||||
install -m755 "$project_dir/target/release/flectar-mail" \
|
||||
"$package_root/usr/bin/flectar-mail"
|
||||
python3 "$project_dir/scripts/stage-pdfium.py" linux-x64 "$package_root/usr/lib/flectar-mail"
|
||||
python3 "$project_dir/scripts/test-pdf-preview.py" "$package_root/usr/bin/flectar-mail"
|
||||
python3 "$project_dir/scripts/stage-linux-metadata.py" "$package_root"
|
||||
install -m644 "$project_dir/resources/app-icon/flectar-mail-masked-512.png" \
|
||||
"$package_root/usr/share/icons/hicolor/512x512/apps/com.flectar.mail.png"
|
||||
@@ -55,10 +57,10 @@ install -m644 "$project_dir/resources/fonts/google-sans-flex/README.md" \
|
||||
cp "$project_dir/resources/debian/source-control.in" "$build_dir/debian/control"
|
||||
shlib_substitution="$(
|
||||
cd "$build_dir"
|
||||
dpkg-shlibdeps -O -edebian/flectar-mail/usr/bin/flectar-mail
|
||||
dpkg-shlibdeps -O -edebian/flectar-mail/usr/bin/flectar-mail -edebian/flectar-mail/usr/lib/flectar-mail/libpdfium.so
|
||||
)"
|
||||
runtime_dependencies="${shlib_substitution#shlibs:Depends=}"
|
||||
runtime_dependencies+=", libfontconfig1, libwayland-client0, libx11-6, libx11-xcb1, libxkbcommon0, libxkbcommon-x11-0, hicolor-icon-theme"
|
||||
runtime_dependencies+=", libfontconfig1, libwayland-client0, libx11-6, libx11-xcb1, libxkbcommon0, libxkbcommon-x11-0, hicolor-icon-theme, xdg-utils"
|
||||
sed \
|
||||
-e "s/@VERSION@/$version/g" \
|
||||
-e "s/@DEPENDS@/$runtime_dependencies/g" \
|
||||
|
||||
@@ -19,6 +19,8 @@ mkdir -p \
|
||||
"$app_dir/Contents/Resources/Licenses/Noto Emoji"
|
||||
cp "$binary_path" "$app_dir/Contents/MacOS/flectar-mail"
|
||||
|
||||
python3 "$project_dir/scripts/stage-pdfium.py" mac-arm64 "$app_dir/Contents/MacOS"
|
||||
|
||||
icon_source="$project_dir/resources/app-icon/flectar-mail-masked.png"
|
||||
iconset_dir="$output_dir/FlectarMail.iconset"
|
||||
rm -rf "$iconset_dir"
|
||||
@@ -58,6 +60,7 @@ sed \
|
||||
# An ad-hoc signature makes the CI artifact internally consistent. Distribution
|
||||
# signing and notarization replace it when Apple credentials are configured.
|
||||
codesign --force --deep --sign - "$app_dir"
|
||||
python3 "$project_dir/scripts/test-pdf-preview.py" "$app_dir/Contents/MacOS/flectar-mail"
|
||||
|
||||
mkdir -p "$output_dir"
|
||||
ditto -c -k --sequesterRsrc --keepParent "$app_dir" "$output_dir/flectar-mail-macos-arm64.zip"
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stage a checksum-pinned, non-V8 PDFium runtime for desktop and mobile app packaging.
|
||||
|
||||
No downloads happen at application startup or during cargo build. Release
|
||||
packaging calls this explicitly; developers can target target/debug.
|
||||
"""
|
||||
import argparse
|
||||
import hashlib
|
||||
import io
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import tarfile
|
||||
import urllib.request
|
||||
|
||||
RELEASE = "chromium/8044"
|
||||
ASSETS = {
|
||||
"linux-x64": ("eb142f416aed3a72fc5a02dbd5884868a16cb99dc0cf53e6bdd64afbf67b05f4", "lib/libpdfium.so"),
|
||||
"mac-arm64": ("61424884d4a7f153b808deba6437848e4400834ce30aaf95d3050da44df8f420", "lib/libpdfium.dylib"),
|
||||
"android-arm64": ("37686e64fa005484d619550a78805500de4c5a6f4df7aa06d8576145bfbee98f", "lib/libpdfium.so"),
|
||||
"android-x64": ("e7e3072b8ad6f36ef58db999c2d91a22fa165a904d69385f13db4537c233d736", "lib/libpdfium.so"),
|
||||
"ios-device-arm64": ("52be735d07b6c498ed8ce68b7f0cb5a209bbe61593c7a86c556756b80abbb938", "lib/libpdfium.dylib"),
|
||||
"ios-simulator-arm64": ("544537fce5257146e0df501c632d490d1aa95768737770f1912c8e23b1dd238d", "lib/libpdfium.dylib"),
|
||||
"ios-simulator-x64": ("8d4e33e415e29894aa98481bc9a9162a42039a1672796db14c9fed9cd545c881", "lib/libpdfium.dylib"),
|
||||
"win-x64": ("78a17d9a5f14467631c26a3ac8741b27a0471ecc05bd6a119b523598160a0537", "bin/pdfium.dll"),
|
||||
}
|
||||
|
||||
|
||||
def stage(target, destination, cache=None):
|
||||
checksum, library = ASSETS[target]
|
||||
url = f"https://github.com/bblanchon/pdfium-binaries/releases/download/{RELEASE}/pdfium-{target}.tgz"
|
||||
cached = cache / f"{checksum}.tgz" if cache else None
|
||||
if cached and cached.is_file():
|
||||
data = cached.read_bytes()
|
||||
else:
|
||||
with urllib.request.urlopen(url, timeout=60) as response:
|
||||
data = response.read(64 * 1024 * 1024 + 1)
|
||||
if hashlib.sha256(data).hexdigest() != checksum:
|
||||
raise ValueError("PDFium archive checksum mismatch")
|
||||
if cached and not cached.is_file():
|
||||
cache.mkdir(parents=True, exist_ok=True)
|
||||
# Concurrent Xcode architecture builds must never observe a partial cache.
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(dir=cache, delete=False) as temp:
|
||||
temp.write(data)
|
||||
temporary = pathlib.Path(temp.name)
|
||||
temporary.replace(cached)
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive:
|
||||
members = {member.name.removeprefix("./"): member for member in archive.getmembers()}
|
||||
for name, member in members.items():
|
||||
if name != library and not name.startswith("licenses/") and name != "LICENSE":
|
||||
continue
|
||||
if not member.isfile() or ".." in pathlib.PurePosixPath(name).parts:
|
||||
raise ValueError("Invalid PDFium archive member")
|
||||
relative = pathlib.Path(library).name if name == library else pathlib.Path("pdfium-licenses") / name
|
||||
output = destination / relative
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with archive.extractfile(member) as source, output.open("wb") as sink:
|
||||
shutil.copyfileobj(source, sink)
|
||||
if not (destination / pathlib.Path(library).name).is_file():
|
||||
raise ValueError("PDFium runtime missing from archive")
|
||||
(destination / "pdfium-licenses" / "build.json").write_text(json.dumps({"release": RELEASE, "sha256": checksum, "url": url}, indent=2) + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("target", choices=ASSETS)
|
||||
parser.add_argument("destination", type=pathlib.Path)
|
||||
parser.add_argument("--cache", type=pathlib.Path, help="Verified archive cache for repeated packaging builds")
|
||||
args = parser.parse_args()
|
||||
stage(args.target, args.destination, args.cache)
|
||||
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline regressions for PDFium staging and native package validation."""
|
||||
import hashlib
|
||||
import importlib.util
|
||||
import io
|
||||
import pathlib
|
||||
import plistlib
|
||||
import struct
|
||||
import sys
|
||||
import tarfile
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from unittest.mock import patch
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
SCRIPTS = pathlib.Path(__file__).resolve().parent
|
||||
|
||||
|
||||
def module(name):
|
||||
spec = importlib.util.spec_from_file_location(name, SCRIPTS / (name + ".py"))
|
||||
result = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(result)
|
||||
return result
|
||||
|
||||
|
||||
stage = module("stage-pdfium")
|
||||
native = module("verify-android-native")
|
||||
framework = module("write-pdfium-framework-plist")
|
||||
|
||||
|
||||
def archive(files):
|
||||
result = io.BytesIO()
|
||||
with tarfile.open(fileobj=result, mode="w:gz") as output:
|
||||
for name, data in files.items():
|
||||
entry = tarfile.TarInfo(name)
|
||||
entry.size = len(data)
|
||||
output.addfile(entry, io.BytesIO(data))
|
||||
return result.getvalue()
|
||||
|
||||
|
||||
def elf(alignment=16384):
|
||||
data = bytearray(120)
|
||||
data[:6] = b"\x7fELF\x02\x01"
|
||||
struct.pack_into("<Q", data, 32, 64)
|
||||
struct.pack_into("<HH", data, 54, 56, 1)
|
||||
struct.pack_into("<I", data, 64, 1)
|
||||
struct.pack_into("<Q", data, 112, alignment)
|
||||
return bytes(data)
|
||||
|
||||
|
||||
class PackagingTests(unittest.TestCase):
|
||||
def test_cached_archive_is_verified_before_writing(self):
|
||||
data = archive({"lib/libpdfium.so": b"library", "LICENSE": b"license", "licenses/notice": b"notice"})
|
||||
checksum = hashlib.sha256(data).hexdigest()
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = pathlib.Path(directory)
|
||||
cache = root / "cache"
|
||||
cache.mkdir()
|
||||
cached = cache / (checksum + ".tgz")
|
||||
cached.write_bytes(data)
|
||||
with patch.dict(stage.ASSETS, {"fixture": (checksum, "lib/libpdfium.so")}), patch.object(stage.urllib.request, "urlopen", side_effect=AssertionError("cached staging must be offline")):
|
||||
stage.stage("fixture", root / "output", cache)
|
||||
self.assertEqual((root / "output/libpdfium.so").read_bytes(), b"library")
|
||||
self.assertTrue((root / "output/pdfium-licenses/build.json").is_file())
|
||||
cached.write_bytes(b"corrupted archive")
|
||||
with self.assertRaisesRegex(ValueError, "checksum mismatch"):
|
||||
stage.stage("fixture", root / "rejected", cache)
|
||||
self.assertFalse((root / "rejected").exists())
|
||||
|
||||
def test_archive_traversal_is_rejected(self):
|
||||
data = archive({"licenses/../../outside": b"unexpected"})
|
||||
checksum = hashlib.sha256(data).hexdigest()
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = pathlib.Path(directory)
|
||||
with patch.dict(stage.ASSETS, {"fixture": (checksum, "lib/libpdfium.so")}), patch.object(stage.urllib.request, "urlopen", return_value=io.BytesIO(data)):
|
||||
with self.assertRaisesRegex(ValueError, "Invalid PDFium archive member"):
|
||||
stage.stage("fixture", root / "output")
|
||||
self.assertFalse((root / "outside").exists())
|
||||
|
||||
def test_apk_requires_pdfium_notices_and_aligned_native_libraries(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
apk = pathlib.Path(directory) / "fixture.apk"
|
||||
files = {
|
||||
"lib/arm64-v8a/libflectar_mail_android.so": elf(),
|
||||
"lib/arm64-v8a/libpdfium.so": elf(),
|
||||
"assets/licenses/PDFium/arm64-v8a/pdfium-licenses/build.json": b"{}",
|
||||
"assets/licenses/PDFium/arm64-v8a/pdfium-licenses/LICENSE": b"license",
|
||||
}
|
||||
def write(values):
|
||||
with zipfile.ZipFile(apk, "w") as output:
|
||||
for name, data in values.items():
|
||||
output.writestr(name, data)
|
||||
write(files)
|
||||
native.verify_apk(apk)
|
||||
write({k: v for k, v in files.items() if not k.endswith("libpdfium.so")})
|
||||
with self.assertRaisesRegex(ValueError, "missing bundled PDFium"):
|
||||
native.verify_apk(apk)
|
||||
write({**files, "lib/arm64-v8a/libpdfium.so": elf(4096)})
|
||||
with self.assertRaisesRegex(ValueError, "not aligned"):
|
||||
native.verify_apk(apk)
|
||||
|
||||
def test_ios_framework_metadata_distinguishes_device_and_simulator(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
path = pathlib.Path(directory) / "Info.plist"
|
||||
for simulator in (False, True):
|
||||
framework.write(path, simulator)
|
||||
info = plistlib.loads(path.read_bytes())
|
||||
self.assertEqual(info["CFBundleExecutable"], "PDFium")
|
||||
self.assertEqual(info["CFBundlePackageType"], "FMWK")
|
||||
self.assertEqual(info["MinimumOSVersion"], "17.0")
|
||||
self.assertEqual(info["CFBundleSupportedPlatforms"], ["iPhoneSimulator" if simulator else "iPhoneOS"])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exercise the packaged PDF worker without initializing UI or account databases.
|
||||
|
||||
Usage: python3 scripts/test-pdf-preview.py target/debug/flectar-mail [output.png]
|
||||
Creates an original two-page vector/text fixture and checks page selection,
|
||||
colors, dimensions, malformed inputs and protocol limits. Requires staged PDFium.
|
||||
"""
|
||||
import pathlib
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import zlib
|
||||
|
||||
|
||||
def fixture():
|
||||
streams = [b"0.1 0.4 0.8 rg 20 20 160 100 re f BT /F1 16 Tf 20 150 Td (Flectar PDF preview) Tj ET", b"0.8 0.2 0.1 rg 20 20 160 100 re f BT /F1 16 Tf 20 150 Td (Second page) Tj ET"]
|
||||
objects = [b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>"]
|
||||
for content in (6, 7):
|
||||
objects.append(f"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 200 200] /Resources << /Font << /F1 5 0 R >> >> /Contents {content} 0 R >>".encode())
|
||||
objects.append(b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>")
|
||||
objects += [b"<< /Length " + str(len(s)).encode() + b" >>\nstream\n" + s + b"\nendstream" for s in streams]
|
||||
data = b"%PDF-1.4\n"; offsets = [0]
|
||||
for i, obj in enumerate(objects, 1):
|
||||
offsets.append(len(data)); data += f"{i} 0 obj\n".encode() + obj + b"\nendobj\n"
|
||||
xref = len(data)
|
||||
data += f"xref\n0 {len(offsets)}\n0000000000 65535 f \n".encode()
|
||||
data += b"".join(f"{offset:010} 00000 n \n".encode() for offset in offsets[1:])
|
||||
return data + f"trailer\n<< /Size {len(offsets)} /Root 1 0 R >>\nstartxref\n{xref}\n%%EOF\n".encode()
|
||||
|
||||
|
||||
def png(path, width, height, pixels):
|
||||
def chunk(kind, value):
|
||||
return struct.pack(">I", len(value)) + kind + value + struct.pack(">I", zlib.crc32(kind + value))
|
||||
raw = b"".join(b"\0" + pixels[y * width * 4:(y + 1) * width * 4] for y in range(height))
|
||||
pathlib.Path(path).write_bytes(b"\x89PNG\r\n\x1a\n" + chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 6, 0, 0, 0)) + chunk(b"IDAT", zlib.compress(raw)) + chunk(b"IEND", b""))
|
||||
|
||||
|
||||
def check(binary, output=None):
|
||||
data = fixture()
|
||||
for index, zoom in [(0, 100), (1, 100), (0, 200)]:
|
||||
start = time.monotonic()
|
||||
result = subprocess.run([binary, "--render-pdf-page"], input=struct.pack("<III", index, zoom, len(data)) + data, capture_output=True, timeout=22)
|
||||
assert result.returncode == 0, (result.returncode, result.stderr.decode(errors="replace"))
|
||||
magic, count, width, height = struct.unpack("<4sIII", result.stdout[:16])
|
||||
pixels = result.stdout[16:]
|
||||
assert magic == b"FPD1" and count == 2
|
||||
assert 0 < width <= 2880 and 0 < height <= 2880
|
||||
assert len(pixels) == width * height * 4
|
||||
center = pixels[(height // 2 * width + width // 2) * 4:][:4]
|
||||
expected = (26, 102, 204, 255) if index == 0 else (204, 51, 26, 255)
|
||||
assert all(abs(a - b) <= 1 for a, b in zip(center, expected)), center
|
||||
print(f"Page {index + 1} at {zoom}%: {width}x{height}, {time.monotonic() - start:.3f}s")
|
||||
if output and index == 0 and zoom == 100: png(output, width, height, pixels)
|
||||
for payload in [struct.pack("<III", 9, 100, len(data)) + data, struct.pack("<III", 0, 100, 4) + b"oops", struct.pack("<III", 0, 100, 16 * 1024 * 1024 + 1), struct.pack("<III", 0, 999, 0), b"short"]:
|
||||
result = subprocess.run([binary, "--render-pdf-page"], input=payload, capture_output=True, timeout=22)
|
||||
assert result.returncode != 0 and not result.stdout
|
||||
print("PASS: pages, zoom, bitmap bounds, colors, malformed input and oversized requests")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if sys.argv[1] == "--fixture":
|
||||
pathlib.Path(sys.argv[2]).write_bytes(fixture())
|
||||
else:
|
||||
check(str(pathlib.Path(sys.argv[1]).resolve()), sys.argv[2] if len(sys.argv) > 2 else None)
|
||||
@@ -145,6 +145,19 @@ class ReleaseTests(unittest.TestCase):
|
||||
(root / "Cargo.toml").write_text('[package]\nversion = "0.1.0-alpha.1"\n')
|
||||
(root / "target/release").mkdir(parents=True)
|
||||
shutil.copyfile("/bin/true", root / "target/release/flectar-mail")
|
||||
# This test checks package metadata using fixture ELFs. Stub only
|
||||
# the download/renderer helpers in the disposable checkout; actual
|
||||
# native PDF rendering is exercised by the dedicated smoke tests.
|
||||
(root / "scripts/stage-pdfium.py").write_text(
|
||||
"import pathlib, shutil, sys\n"
|
||||
"destination = pathlib.Path(sys.argv[2])\n"
|
||||
"destination.mkdir(parents=True, exist_ok=True)\n"
|
||||
"shutil.copyfile('/bin/true', destination / 'libpdfium.so')\n"
|
||||
)
|
||||
(root / "scripts/test-pdf-preview.py").write_text(
|
||||
"import pathlib, sys\n"
|
||||
"assert pathlib.Path(sys.argv[1]).is_file()\n"
|
||||
)
|
||||
(root / "bin").mkdir()
|
||||
cargo = root / "bin/cargo"
|
||||
cargo.write_text("#!/bin/sh\nexit 0\n")
|
||||
|
||||
@@ -71,6 +71,8 @@ if [[ "$archive_listing" != *'classes.dex'* ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
python3 "$project_dir/scripts/verify-android-native.py" "$apk"
|
||||
|
||||
signing="$($apksigner verify --verbose --print-certs "$apk")"
|
||||
printf '%s\n' "$signing"
|
||||
signing_args=("$signing_mode")
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify packaged native ABI completeness and 16 KiB ELF load alignment."""
|
||||
import argparse
|
||||
import struct
|
||||
import zipfile
|
||||
|
||||
|
||||
def verify_elf(data, name):
|
||||
if data[:6] != b"\x7fELF\x02\x01":
|
||||
raise ValueError(f"{name}: expected little-endian ELF64")
|
||||
offset = struct.unpack_from("<Q", data, 32)[0]
|
||||
entry_size, count = struct.unpack_from("<HH", data, 54)
|
||||
if entry_size < 56 or count == 0 or offset + count * entry_size > len(data):
|
||||
raise ValueError(f"{name}: invalid ELF program headers")
|
||||
loads = 0
|
||||
for i in range(count):
|
||||
entry = offset + i * entry_size
|
||||
if struct.unpack_from("<I", data, entry)[0] != 1:
|
||||
continue
|
||||
file_offset, address = struct.unpack_from("<QQ", data, entry + 8)
|
||||
alignment = struct.unpack_from("<Q", data, entry + 48)[0]
|
||||
if alignment < 16384 or file_offset % 16384 != address % 16384:
|
||||
raise ValueError(f"{name}: PT_LOAD is not aligned for 16 KiB Android pages")
|
||||
loads += 1
|
||||
if not loads:
|
||||
raise ValueError(f"{name}: missing ELF load segments")
|
||||
|
||||
|
||||
def verify_apk(path):
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
names = set(archive.namelist())
|
||||
app_libraries = sorted(name for name in names if name.endswith("/libflectar_mail_android.so"))
|
||||
if not app_libraries:
|
||||
raise ValueError("APK contains no Flectar native application")
|
||||
for app in app_libraries:
|
||||
abi = app.split("/")[1]
|
||||
pdfium = f"lib/{abi}/libpdfium.so"
|
||||
provenance = f"assets/licenses/PDFium/{abi}/pdfium-licenses/build.json"
|
||||
license_name = f"assets/licenses/PDFium/{abi}/pdfium-licenses/LICENSE"
|
||||
if not {pdfium, provenance, license_name} <= names:
|
||||
raise ValueError(f"{abi}: missing bundled PDFium runtime, provenance or license")
|
||||
for name in sorted(names):
|
||||
if name.startswith("lib/") and name.endswith(".so"):
|
||||
verify_elf(archive.read(name), name)
|
||||
print("PASS: native libraries include PDFium and licenses, with 16 KiB ELF alignment")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("apk")
|
||||
verify_apk(parser.parse_args().apk)
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Write the metadata for our embedded, signed iOS PDFium framework."""
|
||||
import pathlib
|
||||
import plistlib
|
||||
import sys
|
||||
|
||||
|
||||
def write(path, simulator):
|
||||
metadata = {
|
||||
"CFBundleDevelopmentRegion": "en",
|
||||
"CFBundleExecutable": "PDFium",
|
||||
"CFBundleIdentifier": "com.flectar.pdfium",
|
||||
"CFBundleInfoDictionaryVersion": "6.0",
|
||||
"CFBundleName": "PDFium",
|
||||
"CFBundlePackageType": "FMWK",
|
||||
"CFBundleShortVersionString": "8044.0.0",
|
||||
"CFBundleVersion": "8044",
|
||||
"CFBundleSupportedPlatforms": ["iPhoneSimulator" if simulator else "iPhoneOS"],
|
||||
"MinimumOSVersion": "17.0",
|
||||
}
|
||||
path.write_bytes(plistlib.dumps(metadata))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
write(pathlib.Path(sys.argv[1]), bool(int(sys.argv[2])))
|
||||
Reference in new issue
Block a user