mirror of
https://github.com/flectar/mail.git
synced 2026-11-11 17:57:54 +00:00
build: publish the iOS app for sideloading
This commit is contained in:
1 parent
9b7853df9e
commit
dc37fbc375
7 files changed
+326
-35
No files matched your search
@@ -24,9 +24,9 @@ on:
|
||||
type: boolean
|
||||
default: true
|
||||
include_ios:
|
||||
description: Also build an experimental unsigned iOS archive
|
||||
description: Build the unsigned iOS app
|
||||
type: boolean
|
||||
default: false
|
||||
default: true
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/native-platforms.yml"
|
||||
@@ -708,15 +708,17 @@ jobs:
|
||||
|
||||
ios:
|
||||
needs: metadata
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.include_ios
|
||||
name: iOS unsigned archive (arm64)
|
||||
if: github.event_name == 'push' || inputs.include_ios
|
||||
name: iOS app (arm64, unsigned)
|
||||
runs-on: macos-15
|
||||
env:
|
||||
SOURCE_DATE_EPOCH: ${{ needs.metadata.outputs.source_date_epoch }}
|
||||
steps:
|
||||
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
|
||||
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: aarch64-apple-ios,aarch64-apple-ios-sim
|
||||
targets: aarch64-apple-ios
|
||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
|
||||
with:
|
||||
cache-on-failure: true
|
||||
@@ -725,34 +727,18 @@ jobs:
|
||||
- name: Install Xcode project generator
|
||||
run: brew install xcodegen
|
||||
|
||||
- name: Generate Xcode project
|
||||
working-directory: platform/ios
|
||||
run: xcodegen generate
|
||||
|
||||
- name: Build unsigned device archive
|
||||
# Unsigned, so people can sideload it with their own Apple ID.
|
||||
- name: Build the IPA
|
||||
# iOS bundles its own OAuth registrations (platform/ios/README.md).
|
||||
env:
|
||||
FLECTAR_GOOGLE_IOS_CLIENT_ID: ${{ secrets.FLECTAR_GOOGLE_IOS_CLIENT_ID }}
|
||||
FLECTAR_MICROSOFT_IOS_CLIENT_ID: ${{ secrets.FLECTAR_MICROSOFT_IOS_CLIENT_ID }}
|
||||
run: |
|
||||
mkdir -p target/ios
|
||||
xcodebuild \
|
||||
-project platform/ios/FlectarMail.xcodeproj \
|
||||
-scheme FlectarMail \
|
||||
-configuration Release \
|
||||
-sdk iphoneos \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-archivePath "$PWD/target/ios/FlectarMail.xcarchive" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
archive
|
||||
ditto -c -k --sequesterRsrc --keepParent \
|
||||
target/ios/FlectarMail.xcarchive \
|
||||
target/ios/flectar-mail-ios-unsigned-xcarchive.zip
|
||||
run: bash scripts/package-ios.sh target/ios
|
||||
|
||||
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
with:
|
||||
name: flectar-mail-ios-arm64-unsigned
|
||||
path: target/ios/flectar-mail-ios-unsigned-xcarchive.zip
|
||||
name: flectar-mail-ios-arm64
|
||||
path: target/ios/flectar-mail-ios-arm64.ipa
|
||||
if-no-files-found: error
|
||||
|
||||
release:
|
||||
@@ -765,8 +751,9 @@ jobs:
|
||||
needs.rpm-test.result == 'success' &&
|
||||
needs.flatpak.result == 'success' && needs.windows.result == 'success' &&
|
||||
needs.microsoft-store.result == 'success' &&
|
||||
needs.macos.result == 'success' && needs.android.result == 'success'
|
||||
needs: [metadata, quality, linux, rpm, rpm-test, flatpak, windows, microsoft-store, macos, android]
|
||||
needs.macos.result == 'success' && needs.android.result == 'success' &&
|
||||
needs.ios.result == 'success'
|
||||
needs: [metadata, quality, linux, rpm, rpm-test, flatpak, windows, microsoft-store, macos, android, ios]
|
||||
runs-on: ubuntu-24.04
|
||||
permissions:
|
||||
actions: read
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
|
||||
@@ -5,6 +5,10 @@ settings:
|
||||
base:
|
||||
ENABLE_USER_SCRIPT_SANDBOXING: NO
|
||||
PRODUCT_BUNDLE_IDENTIFIER: com.flectar.mail
|
||||
# scripts/package-ios.sh sets both from Cargo.toml; these values only
|
||||
# apply to builds started from Xcode.
|
||||
MARKETING_VERSION: "0.1.0"
|
||||
CURRENT_PROJECT_VERSION: "1"
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME: AppIcon
|
||||
# Alternate icons chosen in Settings; resources/app-icon/generate.py
|
||||
# writes their icon sets and checks this list.
|
||||
@@ -18,6 +22,8 @@ targets:
|
||||
path: Info.plist
|
||||
properties:
|
||||
CFBundleDisplayName: Flectar Mail
|
||||
CFBundleShortVersionString: $(MARKETING_VERSION)
|
||||
CFBundleVersion: $(CURRENT_PROJECT_VERSION)
|
||||
NSLocalNetworkUsageDescription: Flectar Mail connects to mail servers on your local network.
|
||||
UILaunchScreen: {}
|
||||
UIRequiresFullScreen: false
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Package the iOS app for sideloading.
|
||||
|
||||
`ipa` turns the unsigned app that scripts/package-ios.sh builds into the IPA
|
||||
that GitHub releases publish.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import plistlib
|
||||
import shutil
|
||||
import stat
|
||||
import time
|
||||
import zipfile
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
BUNDLE_ID = "com.flectar.mail"
|
||||
# The earliest time a ZIP entry can record.
|
||||
ZIP_EPOCH = 315532800
|
||||
|
||||
|
||||
def check_app(app: Path, short_version: str, build_version: str) -> list[Path]:
|
||||
"""Check the app's identity and that it is unsigned; return its contents."""
|
||||
info = plistlib.loads((app / "Info.plist").read_bytes())
|
||||
for key, expected in (
|
||||
("CFBundleIdentifier", BUNDLE_ID),
|
||||
("CFBundleShortVersionString", short_version),
|
||||
("CFBundleVersion", build_version),
|
||||
):
|
||||
if info.get(key) != expected:
|
||||
raise ValueError(f"{key} is {info.get(key)!r}, expected {expected!r}")
|
||||
if not info.get("MinimumOSVersion"):
|
||||
raise ValueError("Info.plist names no MinimumOSVersion")
|
||||
executable = app / info.get("CFBundleExecutable", "")
|
||||
if executable == app or not executable.is_file() or not executable.stat().st_mode & stat.S_IXUSR:
|
||||
raise ValueError("The app's CFBundleExecutable is missing or not executable")
|
||||
contents = sorted(app.rglob("*"))
|
||||
for path in contents:
|
||||
if path.is_symlink():
|
||||
raise ValueError(f"{path.relative_to(app)} is a symbolic link")
|
||||
# Sideloading tools sign the app with the user's Apple ID. Anything
|
||||
# signed here would need its entitlements declared in the AltStore
|
||||
# source.
|
||||
if path.name in ("_CodeSignature", "embedded.mobileprovision"):
|
||||
raise ValueError(f"{path.relative_to(app)} is signing material; the IPA must be unsigned")
|
||||
return contents
|
||||
|
||||
|
||||
def write_ipa(app: Path, output: Path, short_version: str, build_version: str) -> None:
|
||||
contents = check_app(app, short_version, build_version)
|
||||
# Entries keep their permissions but share one timestamp, so the same app
|
||||
# always packages into the same IPA.
|
||||
epoch = max(int(os.environ.get("SOURCE_DATE_EPOCH") or ZIP_EPOCH), ZIP_EPOCH)
|
||||
date_time = time.gmtime(epoch)[:6]
|
||||
partial = output.with_name(output.name + ".partial")
|
||||
try:
|
||||
with zipfile.ZipFile(partial, "w", zipfile.ZIP_DEFLATED) as ipa:
|
||||
payload = zipfile.ZipInfo("Payload/", date_time)
|
||||
payload.external_attr = (stat.S_IFDIR | 0o755) << 16 | 0x10
|
||||
ipa.writestr(payload, b"")
|
||||
for path in (app, *contents):
|
||||
name = PurePosixPath("Payload", app.name, path.relative_to(app).as_posix())
|
||||
entry = zipfile.ZipInfo.from_file(path, str(name), strict_timestamps=False)
|
||||
entry.date_time = date_time
|
||||
if path.is_dir():
|
||||
ipa.writestr(entry, b"")
|
||||
continue
|
||||
entry.compress_type = zipfile.ZIP_DEFLATED
|
||||
with path.open("rb") as source, ipa.open(entry, "w") as target:
|
||||
shutil.copyfileobj(source, target)
|
||||
partial.replace(output)
|
||||
finally:
|
||||
partial.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
commands = parser.add_subparsers(dest="command", required=True)
|
||||
ipa = commands.add_parser("ipa", help="package the unsigned app as an IPA")
|
||||
ipa.add_argument("app", type=Path)
|
||||
ipa.add_argument("output", type=Path)
|
||||
ipa.add_argument("--short-version", required=True)
|
||||
ipa.add_argument("--build-version", required=True)
|
||||
args = parser.parse_args()
|
||||
try:
|
||||
write_ipa(args.app, args.output, args.short_version, args.build_version)
|
||||
except (OSError, ValueError) as error:
|
||||
raise SystemExit(str(error)) from error
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build the unsigned iOS app and package it as an IPA for sideloading.
|
||||
# AltStore, SideStore and Sideloadly sign it with the user's own Apple ID.
|
||||
set -euo pipefail
|
||||
|
||||
output_dir="${1:?output directory is required}"
|
||||
project_dir="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||
mkdir -p "$output_dir"
|
||||
output_dir="$(CDPATH='' cd -- "$output_dir" && pwd)"
|
||||
|
||||
version="$(sed -n 's/^version = "\([^"]*\)"/\1/p' "$project_dir/Cargo.toml" | head -1)"
|
||||
# CFBundleShortVersionString takes only numbers, so prereleases share it.
|
||||
# CFBundleVersion follows the Android version sequence (alpha < beta < rc <
|
||||
# release), which AltStore and SideStore compare to offer updates.
|
||||
short_version="${version%%-*}"
|
||||
build_version="$(python3 "$project_dir/scripts/android-version-code.py" "$version")"
|
||||
|
||||
xcodegen generate --spec "$project_dir/platform/ios/project.yml"
|
||||
archive="$output_dir/FlectarMail.xcarchive"
|
||||
rm -rf "$archive"
|
||||
xcodebuild \
|
||||
-project "$project_dir/platform/ios/FlectarMail.xcodeproj" \
|
||||
-scheme FlectarMail \
|
||||
-configuration Release \
|
||||
-sdk iphoneos \
|
||||
-destination 'generic/platform=iOS' \
|
||||
-archivePath "$archive" \
|
||||
CODE_SIGNING_ALLOWED=NO \
|
||||
MARKETING_VERSION="$short_version" \
|
||||
CURRENT_PROJECT_VERSION="$build_version" \
|
||||
archive
|
||||
|
||||
# A signature would only be replaced, and entitlements would have to be
|
||||
# declared in the AltStore source.
|
||||
app="$archive/Products/Applications/FlectarMail.app"
|
||||
for code in "$app" "$app"/Frameworks/*.framework; do
|
||||
if codesign --display "$code" 2>/dev/null; then
|
||||
printf '%s is signed; the IPA must be unsigned.\n' "$code" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
python3 "$project_dir/scripts/ios-release.py" ipa "$app" "$output_dir/flectar-mail-ios-arm64.ipa" \
|
||||
--short-version "$short_version" --build-version "$build_version"
|
||||
printf 'Built %s (%s, build %s)\n' "$output_dir/flectar-mail-ios-arm64.ipa" "$version" "$build_version"
|
||||
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Stage desktop and Android downloads with checksums."""
|
||||
"""Stage desktop and mobile downloads with checksums."""
|
||||
|
||||
import hashlib
|
||||
import os
|
||||
@@ -31,6 +31,7 @@ def prepare(source: Path, destination: Path, version: str) -> None:
|
||||
("flectar-mail-macos-arm64", "flectar-mail-macos-arm64.dmg", f"flectar-mail-{version}-macos-arm64.dmg"),
|
||||
("flectar-mail-macos-x64", "flectar-mail-macos-x64.zip", f"flectar-mail-{version}-macos-x64.zip"),
|
||||
("flectar-mail-macos-x64", "flectar-mail-macos-x64.dmg", f"flectar-mail-{version}-macos-x64.dmg"),
|
||||
("flectar-mail-ios-arm64", "flectar-mail-ios-arm64.ipa", f"flectar-mail-{version}-ios-arm64.ipa"),
|
||||
("flectar-mail-android-signed-arm64", "flectar-mail.apk", f"flectar-mail-{version}-android-arm64.apk"),
|
||||
)
|
||||
# Validate every input before copying anything; unrelated benchmark files
|
||||
@@ -81,8 +82,10 @@ def release_notes(version: str) -> str:
|
||||
"installation. The preview Flatpak is a sideloaded bundle and does not "
|
||||
"receive Flathub updates yet; close-to-tray is disabled in that package. "
|
||||
"SHA256SUMS covers every download, and GitHub "
|
||||
"records signed build-provenance attestations. iOS archives are "
|
||||
"experimental and available through manual workflow runs only.\n\n"
|
||||
"records signed build-provenance attestations.\n\n"
|
||||
"The iOS app (iOS 17 or later) is unsigned. Sideload it with AltStore, "
|
||||
"SideStore, or Sideloadly, which sign it with your Apple ID; see "
|
||||
"https://github.com/flectar/mail/blob/main/docs/install.md#ios.\n\n"
|
||||
+ android
|
||||
)
|
||||
|
||||
|
||||
+160
-2
@@ -6,6 +6,7 @@ import hashlib
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import plistlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -13,6 +14,7 @@ import tempfile
|
||||
import tomllib
|
||||
import unittest
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
@@ -34,6 +36,7 @@ android_signing = load_script("verify-android-signing")
|
||||
verify_android_signing = android_signing.verify
|
||||
android_version_code = load_script("android-version-code").version_code
|
||||
linux_metadata = load_script("stage-linux-metadata")
|
||||
ios_release = load_script("ios-release")
|
||||
stage_linux_metadata = linux_metadata.stage
|
||||
|
||||
|
||||
@@ -684,6 +687,158 @@ else:
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("pdfium.dll is missing", result.stderr)
|
||||
|
||||
def test_ios_app_packages_into_an_unsigned_ipa(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
app = root / "FlectarMail.app"
|
||||
(app / "Frameworks/PDFium.framework").mkdir(parents=True)
|
||||
(app / "Licenses/Flectar Mail").mkdir(parents=True)
|
||||
info = {
|
||||
"CFBundleExecutable": "FlectarMail",
|
||||
"CFBundleIdentifier": "com.flectar.mail",
|
||||
"CFBundleShortVersionString": "0.1.0",
|
||||
"CFBundleVersion": "1000008",
|
||||
"MinimumOSVersion": "17.0",
|
||||
}
|
||||
# Xcode writes the built Info.plist in binary form.
|
||||
(app / "Info.plist").write_bytes(plistlib.dumps(info, fmt=plistlib.FMT_BINARY))
|
||||
for name, mode in (
|
||||
("FlectarMail", 0o755),
|
||||
("Frameworks/PDFium.framework/PDFium", 0o755),
|
||||
("Licenses/Flectar Mail/LICENSE", 0o644),
|
||||
):
|
||||
(app / name).write_bytes(name.encode())
|
||||
(app / name).chmod(mode)
|
||||
ipa = root / "app.ipa"
|
||||
with patch.dict(os.environ, {"SOURCE_DATE_EPOCH": "1767225600"}):
|
||||
ios_release.write_ipa(app, ipa, "0.1.0", "1000008")
|
||||
first = ipa.read_bytes()
|
||||
(app / "FlectarMail").touch()
|
||||
ios_release.write_ipa(app, ipa, "0.1.0", "1000008")
|
||||
# The same app always packages into the same IPA.
|
||||
self.assertEqual(ipa.read_bytes(), first)
|
||||
self.assertEqual(list(root.glob("*.partial")), [])
|
||||
with zipfile.ZipFile(ipa) as archive:
|
||||
entries = {entry.filename: entry for entry in archive.infolist()}
|
||||
self.assertEqual(plistlib.loads(archive.read("Payload/FlectarMail.app/Info.plist")), info)
|
||||
self.assertEqual(sorted(entries), [
|
||||
"Payload/",
|
||||
"Payload/FlectarMail.app/",
|
||||
"Payload/FlectarMail.app/FlectarMail",
|
||||
"Payload/FlectarMail.app/Frameworks/",
|
||||
"Payload/FlectarMail.app/Frameworks/PDFium.framework/",
|
||||
"Payload/FlectarMail.app/Frameworks/PDFium.framework/PDFium",
|
||||
"Payload/FlectarMail.app/Info.plist",
|
||||
"Payload/FlectarMail.app/Licenses/",
|
||||
"Payload/FlectarMail.app/Licenses/Flectar Mail/",
|
||||
"Payload/FlectarMail.app/Licenses/Flectar Mail/LICENSE",
|
||||
])
|
||||
# Executables stay executable.
|
||||
for name, mode in (
|
||||
("FlectarMail", 0o755),
|
||||
("Frameworks/PDFium.framework/PDFium", 0o755),
|
||||
("Licenses/Flectar Mail/LICENSE", 0o644),
|
||||
):
|
||||
self.assertEqual(entries[f"Payload/FlectarMail.app/{name}"].external_attr >> 16 & 0o777, mode)
|
||||
self.assertEqual({entry.date_time for entry in entries.values()}, {(2026, 1, 1, 0, 0, 0)})
|
||||
|
||||
# Another identity or version, or a missing executable, is refused.
|
||||
for key, value in (
|
||||
("CFBundleIdentifier", "com.example.mail"),
|
||||
("CFBundleShortVersionString", "1.0"),
|
||||
("CFBundleVersion", "1"),
|
||||
("MinimumOSVersion", None),
|
||||
("CFBundleExecutable", "Missing"),
|
||||
("CFBundleExecutable", None),
|
||||
):
|
||||
with self.subTest(key=key, value=value):
|
||||
changed = {**info, key: value}
|
||||
(app / "Info.plist").write_bytes(plistlib.dumps({k: v for k, v in changed.items() if v is not None}))
|
||||
with self.assertRaises(ValueError):
|
||||
ios_release.write_ipa(app, root / "refused.ipa", "0.1.0", "1000008")
|
||||
(app / "Info.plist").write_bytes(plistlib.dumps(info))
|
||||
(app / "FlectarMail").chmod(0o644)
|
||||
with self.assertRaises(ValueError):
|
||||
ios_release.write_ipa(app, root / "refused.ipa", "0.1.0", "1000008")
|
||||
(app / "FlectarMail").chmod(0o755)
|
||||
# Sideloading tools sign the app; signing material and symbolic
|
||||
# links would not survive that.
|
||||
for name in ("_CodeSignature", "Frameworks/PDFium.framework/_CodeSignature", "embedded.mobileprovision"):
|
||||
with self.subTest(name=name):
|
||||
(app / name).mkdir()
|
||||
with self.assertRaisesRegex(ValueError, "unsigned"):
|
||||
ios_release.write_ipa(app, root / "refused.ipa", "0.1.0", "1000008")
|
||||
(app / name).rmdir()
|
||||
(app / "Frameworks/link").symlink_to("../FlectarMail")
|
||||
with self.assertRaisesRegex(ValueError, "symbolic link"):
|
||||
ios_release.write_ipa(app, root / "refused.ipa", "0.1.0", "1000008")
|
||||
(app / "Frameworks/link").unlink()
|
||||
self.assertFalse((root / "refused.ipa").exists())
|
||||
ios_release.write_ipa(app, root / "accepted.ipa", "0.1.0", "1000008")
|
||||
|
||||
def test_ios_packaging_stamps_the_release_version(self):
|
||||
# Run the real script with stand-ins for the Xcode tools, which only
|
||||
# exist on macOS.
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
(root / "scripts").mkdir()
|
||||
for name in ("package-ios.sh", "ios-release.py", "android-version-code.py"):
|
||||
shutil.copyfile(SCRIPTS / name, root / "scripts" / name)
|
||||
(root / "Cargo.toml").write_text('[package]\nname = "flectar-mail"\nversion = "0.1.0-alpha.8"\n')
|
||||
tools = root / "bin"
|
||||
tools.mkdir()
|
||||
calls = root / "calls.log"
|
||||
(tools / "xcodegen").write_text(f'#!/bin/sh\necho "xcodegen $*" >> "{calls}"\n')
|
||||
# Build an app the way Xcode would with the given settings.
|
||||
(tools / "xcodebuild").write_text(f"""#!/usr/bin/env python3
|
||||
import os, plistlib, sys
|
||||
from pathlib import Path
|
||||
with open({str(calls)!r}, "a") as log:
|
||||
log.write("xcodebuild " + " ".join(sys.argv[1:]) + "\\n")
|
||||
arguments = sys.argv[1:]
|
||||
settings = dict(argument.split("=", 1) for argument in arguments if "=" in argument)
|
||||
app = Path(arguments[arguments.index("-archivePath") + 1], "Products/Applications/FlectarMail.app")
|
||||
(app / "Frameworks/PDFium.framework").mkdir(parents=True)
|
||||
for name in ("FlectarMail", "Frameworks/PDFium.framework/PDFium"):
|
||||
(app / name).write_bytes(b"binary")
|
||||
(app / name).chmod(0o755)
|
||||
(app / "Info.plist").write_bytes(plistlib.dumps({{
|
||||
"CFBundleExecutable": "FlectarMail",
|
||||
"CFBundleIdentifier": "com.flectar.mail",
|
||||
"CFBundleShortVersionString": settings["MARKETING_VERSION"],
|
||||
"CFBundleVersion": settings["CURRENT_PROJECT_VERSION"],
|
||||
"MinimumOSVersion": "17.0",
|
||||
}}, fmt=plistlib.FMT_BINARY))
|
||||
""")
|
||||
# `codesign --display` fails for unsigned code.
|
||||
(tools / "codesign").write_text('#!/bin/sh\n[ "${FAKE_SIGNED:-}" = 1 ]\n')
|
||||
for tool in tools.iterdir():
|
||||
tool.chmod(0o755)
|
||||
environment = {**os.environ, "PATH": f"{tools}:{os.environ['PATH']}"}
|
||||
environment.pop("SOURCE_DATE_EPOCH", None)
|
||||
result = subprocess.run(
|
||||
["bash", "scripts/package-ios.sh", "out"], cwd=root, env=environment,
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
with zipfile.ZipFile(root / "out/flectar-mail-ios-arm64.ipa") as archive:
|
||||
info = plistlib.loads(archive.read("Payload/FlectarMail.app/Info.plist"))
|
||||
# Prereleases share the numeric version; the build number orders them.
|
||||
self.assertEqual(info["CFBundleShortVersionString"], "0.1.0")
|
||||
self.assertEqual(info["CFBundleVersion"], str(android_version_code("0.1.0-alpha.8")))
|
||||
log = calls.read_text()
|
||||
self.assertIn(f"xcodegen generate --spec {root}/platform/ios/project.yml", log)
|
||||
self.assertIn("CODE_SIGNING_ALLOWED=NO", log)
|
||||
self.assertIn(f"-archivePath {root}/out/FlectarMail.xcarchive", log)
|
||||
|
||||
result = subprocess.run(
|
||||
["bash", "scripts/package-ios.sh", "signed"], cwd=root,
|
||||
env={**environment, "FAKE_SIGNED": "1"}, capture_output=True, text=True,
|
||||
)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("must be unsigned", result.stderr)
|
||||
self.assertFalse((root / "signed/flectar-mail-ios-arm64.ipa").exists())
|
||||
|
||||
def test_staging_and_checksums(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
@@ -707,6 +862,7 @@ else:
|
||||
"flectar-mail-macos-arm64/flectar-mail-macos-arm64.dmg",
|
||||
"flectar-mail-macos-x64/flectar-mail-macos-x64.zip",
|
||||
"flectar-mail-macos-x64/flectar-mail-macos-x64.dmg",
|
||||
"flectar-mail-ios-arm64/flectar-mail-ios-arm64.ipa",
|
||||
"flectar-mail-android-signed-arm64/flectar-mail.apk",
|
||||
)
|
||||
for name in names:
|
||||
@@ -717,8 +873,9 @@ else:
|
||||
(source / "flectar-mail-linux-x64/startup-benchmark/benchmark.json").write_text("{}")
|
||||
dist = root / "dist"
|
||||
prepare(source, dist, "0.1.0-beta.1")
|
||||
self.assertEqual(len(list(dist.iterdir())), 18)
|
||||
self.assertEqual(len(list(dist.iterdir())), 19)
|
||||
self.assertTrue((dist / "flectar-mail-0.1.0-beta.1-android-arm64.apk").is_file())
|
||||
self.assertTrue((dist / "flectar-mail-0.1.0-beta.1-ios-arm64.ipa").is_file())
|
||||
# Each download comes from its own architecture's artifact.
|
||||
for download, name in (
|
||||
("flectar-mail-0.1.0-beta.1-linux-x64.AppImage", names[0]),
|
||||
@@ -769,7 +926,7 @@ else:
|
||||
(folder / f"flectar-mail_0.1.0~beta.1_{arch}.deb").rename(folder / f"flectar-mail_0.1.0_{arch}.deb")
|
||||
stable_dist = root / "stable"
|
||||
prepare(source, stable_dist, "0.1.0")
|
||||
self.assertEqual(len(list(stable_dist.iterdir())), 18)
|
||||
self.assertEqual(len(list(stable_dist.iterdir())), 19)
|
||||
self.assertTrue((stable_dist / "flectar-mail_0.1.0_amd64.deb").is_file())
|
||||
self.assertTrue((stable_dist / "flectar-mail_0.1.0_arm64.deb").is_file())
|
||||
self.assertTrue((stable_dist / "flectar-mail-0.1.0-android-arm64.apk").is_file())
|
||||
@@ -785,6 +942,7 @@ else:
|
||||
self.assertIn("Apple silicon and Intel", release_assets.release_notes("0.1.0-beta.1"))
|
||||
self.assertIn("Linux and Windows on x64 and arm64", release_assets.release_notes("0.1.0-beta.1"))
|
||||
self.assertIn("-linux-arm64.rpm", release_assets.release_notes("0.1.0-beta.1"))
|
||||
self.assertIn("Sideload it with AltStore", release_assets.release_notes("0.1.0-beta.1"))
|
||||
|
||||
def test_metadata_rejects_stale_android_lockfile(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
|
||||
Reference in new issue
Block a user