diff --git a/native/build-ios.mjs b/native/build-ios.mjs
new file mode 100644
index 0000000..d12280c
--- /dev/null
+++ b/native/build-ios.mjs
@@ -0,0 +1,509 @@
+import { Buffer } from 'node:buffer';
+import { spawnSync } from 'node:child_process';
+import { createHash } from 'node:crypto';
+import {
+ copyFileSync,
+ existsSync,
+ mkdirSync,
+ mkdtempSync,
+ readFileSync,
+ realpathSync,
+ renameSync,
+ rmSync,
+ writeFileSync,
+} from 'node:fs';
+import { homedir } from 'node:os';
+import { dirname, isAbsolute, join, relative } from 'node:path';
+import process from 'node:process';
+import { fileURLToPath, pathToFileURL } from 'node:url';
+import { parseArgs } from 'node:util';
+
+export const appleTeamId = 'ZZX2KZGTHU';
+export const bundleIdentifier = 'de.kanbangit.ios';
+
+const plistReader = `import base64, datetime, json, plistlib, sys
+def encode(value):
+ if isinstance(value, bytes): return base64.b64encode(value).decode()
+ if isinstance(value, datetime.datetime): return value.isoformat() + 'Z'
+ raise TypeError(type(value).__name__)
+print(json.dumps(plistlib.loads(sys.stdin.buffer.read()), default=encode))`;
+
+function execute(command, args, options) {
+ const result = spawnSync(command, args, {
+ cwd: options.cwd,
+ env: options.env,
+ input: options.input,
+ encoding: 'utf8',
+ maxBuffer: 32 * 1024 * 1024,
+ stdio: options.inherit ? 'inherit' : 'pipe',
+ });
+ if (result.error || result.status !== 0) {
+ throw new Error(
+ `${options.step} failed: ${result.error?.message || result.stderr?.trim() || `exit ${result.status ?? result.signal}`}`,
+ );
+ }
+ return result.stdout?.trim() || '';
+}
+
+export function validateProfile(profile, mode, now = new Date()) {
+ const entitlements = profile.Entitlements ?? {};
+ if (
+ profile.TeamIdentifier?.length !== 1 ||
+ profile.TeamIdentifier[0] !== appleTeamId ||
+ entitlements['com.apple.developer.team-identifier'] !== appleTeamId ||
+ entitlements['application-identifier'] !==
+ `${appleTeamId}.${bundleIdentifier}`
+ ) {
+ throw new Error(
+ `The profile must explicitly match ${bundleIdentifier} in team ${appleTeamId}.`,
+ );
+ }
+ if (
+ !/^[a-f\d]{8}-(?:[a-f\d]{4}-){3}[a-f\d]{12}$/i.test(profile.UUID) ||
+ !(new Date(profile.ExpirationDate) > now) ||
+ !profile.DeveloperCertificates?.length ||
+ !profile.Platform?.includes('iOS')
+ ) {
+ throw new Error(
+ 'The iOS profile is invalid or expired. Download a current profile from your Apple account.',
+ );
+ }
+ if (mode === 'testflight') {
+ if (
+ entitlements['get-task-allow'] !== false ||
+ entitlements['beta-reports-active'] !== true ||
+ 'ProvisionedDevices' in profile ||
+ profile.ProvisionsAllDevices
+ ) {
+ throw new Error(
+ 'TestFlight requires an App Store Connect profile, not a development, Ad Hoc or enterprise profile.',
+ );
+ }
+ } else if (
+ entitlements['get-task-allow'] !== true ||
+ !profile.ProvisionedDevices?.length ||
+ profile.ProvisionsAllDevices
+ ) {
+ throw new Error(
+ 'Device development requires an iOS App Development profile containing your registered device.',
+ );
+ }
+ return profile;
+}
+
+export function selectIdentity(profile, identities, mode) {
+ const fingerprints = profile.DeveloperCertificates.map((certificate) =>
+ createHash('sha1')
+ .update(Buffer.from(certificate, 'base64'))
+ .digest('hex')
+ .toUpperCase(),
+ );
+ const prefix =
+ mode === 'testflight' ? 'Apple Distribution: ' : 'Apple Development: ';
+ const matches = [
+ ...identities.matchAll(/\) ([A-Fa-f\d]{40}) "([^"]+)"/g),
+ ].filter(
+ ([, fingerprint, name]) =>
+ fingerprints.includes(fingerprint.toUpperCase()) &&
+ name.startsWith(prefix),
+ );
+ if (matches.length !== 1) {
+ throw new Error(
+ `The profile must match exactly one usable ${prefix.replace(': ', '')} certificate with its private key in your local keychain. Select a profile containing the intended certificate.`,
+ );
+ }
+ return matches[0][1].toUpperCase();
+}
+
+export function buildIos({
+ mode = 'testflight',
+ env = process.env,
+ platform = process.platform,
+ directory = dirname(fileURLToPath(import.meta.url)),
+ home = homedir(),
+ run = execute,
+ log = console.log,
+ now = new Date(),
+} = {}) {
+ if (!['testflight', 'development', 'simulator'].includes(mode)) {
+ throw new Error('Choose --mode testflight, development or simulator.');
+ }
+ if (platform !== 'darwin') {
+ throw new Error(
+ 'iOS builds require a Mac with Xcode, XcodeGen and the matching Rust target installed permanently.',
+ );
+ }
+ if (
+ env.APPLE_DEVELOPMENT_TEAM &&
+ env.APPLE_DEVELOPMENT_TEAM !== appleTeamId
+ ) {
+ throw new Error(`APPLE_DEVELOPMENT_TEAM must be ${appleTeamId}.`);
+ }
+ if (env.TAURI_CONFIG) {
+ throw new Error(
+ 'Remove TAURI_CONFIG overrides before building the configured iOS app.',
+ );
+ }
+ const config = JSON.parse(
+ readFileSync(join(directory, 'src-tauri/tauri.conf.json'), 'utf8'),
+ );
+ const ios = JSON.parse(
+ readFileSync(join(directory, 'src-tauri/tauri.ios.conf.json'), 'utf8'),
+ );
+ const version = config.version;
+ const buildNumber = ios.bundle?.iOS?.bundleVersion;
+ if (
+ ios.identifier !== bundleIdentifier ||
+ ios.bundle?.iOS?.developmentTeam !== appleTeamId ||
+ config.identifier !== 'de.kanbangit.desktop' ||
+ config.productName !== 'KanbanGit' ||
+ !/^\d+\.\d+\.\d+$/.test(version) ||
+ !/^[1-9]\d*$/.test(buildNumber)
+ ) {
+ throw new Error(
+ 'Check the iOS team, bundle identifier, version and positive integer bundleVersion in the Tauri configuration.',
+ );
+ }
+ const simulator = mode === 'simulator';
+ const configuration = mode === 'testflight' ? 'release' : 'debug';
+ const target = simulator ? 'aarch64-apple-ios-sim' : 'aarch64-apple-ios';
+ const sdk = simulator ? 'iphonesimulator' : 'iphoneos';
+ const project = join(directory, 'src-tauri/gen/apple');
+ const targetDirectory = join(directory, 'src-tauri/target');
+ const outputRoot = join(targetDirectory, `ios-${mode}`);
+ const destination = join(outputRoot, `${version}-${buildNumber}`);
+ if (mode === 'testflight' && existsSync(destination)) {
+ throw new Error(
+ `Build output already exists: ${destination}. Move it aside, or increment bundle.iOS.bundleVersion for a new TestFlight upload.`,
+ );
+ }
+ const buildEnv = Object.fromEntries(
+ Object.entries(env).filter(
+ ([key]) =>
+ !key.startsWith('APPLE_') &&
+ !key.startsWith('IOS_') &&
+ !key.startsWith('TAURI_') &&
+ ![
+ 'SDKROOT',
+ 'CARGO_ENCODED_RUSTFLAGS',
+ 'RUSTFLAGS',
+ 'RUSTC_WRAPPER',
+ 'RUSTC_WORKSPACE_WRAPPER',
+ 'XCODE_XCCONFIG_FILE',
+ ].includes(key),
+ ),
+ );
+ function step(label, command, args, options = {}) {
+ log(label);
+ return run(command, args, {
+ cwd: directory,
+ env: buildEnv,
+ step: label,
+ ...options,
+ });
+ }
+ function parsePlist(xml) {
+ return JSON.parse(
+ step(
+ 'Reading local plist metadata',
+ 'python3',
+ ['-c', plistReader],
+ { input: xml },
+ ),
+ );
+ }
+ function readProfile(path) {
+ return validateProfile(
+ parsePlist(
+ step('Reading provisioning profile', 'security', [
+ 'cms',
+ '-D',
+ '-i',
+ path,
+ ]),
+ ),
+ mode,
+ now,
+ );
+ }
+ step(
+ 'Checking XcodeGen (install permanently with brew install xcodegen if missing)',
+ 'xcodegen',
+ ['--version'],
+ );
+ step('Checking Xcode', 'xcodebuild', ['-version']);
+ const installedTargets = step('Checking installed Rust targets', 'rustup', [
+ 'target',
+ 'list',
+ '--installed',
+ ]);
+ if (!installedTargets.split(/\s+/).includes(target)) {
+ throw new Error(
+ `Install the missing target permanently with rustup target add ${target}, then retry.`,
+ );
+ }
+ const cargo = step('Selecting the rustup Cargo toolchain', 'rustup', [
+ 'which',
+ 'cargo',
+ ]);
+ buildEnv.RUSTC = step('Selecting the matching Rust compiler', 'rustup', [
+ 'which',
+ 'rustc',
+ ]);
+ buildEnv.PATH = `${dirname(cargo)}:${buildEnv.PATH ?? ''}`;
+ buildEnv.SDKROOT = step('Checking the iOS SDK', 'xcrun', [
+ '--sdk',
+ sdk,
+ '--show-sdk-path',
+ ]);
+ buildEnv.IPHONEOS_DEPLOYMENT_TARGET = ios.bundle.iOS.minimumSystemVersion;
+ buildEnv.CARGO_TARGET_DIR = targetDirectory;
+ let profile;
+ let profilePath;
+ let identity;
+ if (!simulator) {
+ if (
+ !env.IOS_PROVISIONING_PROFILE ||
+ !existsSync(env.IOS_PROVISIONING_PROFILE)
+ ) {
+ throw new Error(
+ 'Set IOS_PROVISIONING_PROFILE to your downloaded .mobileprovision file outside the repository. Apple account setup is manual; this command never creates or downloads profiles.',
+ );
+ }
+ profilePath = realpathSync(env.IOS_PROVISIONING_PROFILE);
+ const withinProject = relative(
+ realpathSync(join(directory, '..')),
+ profilePath,
+ );
+ if (
+ !withinProject.startsWith(
+ `..${process.platform === 'win32' ? '\\' : '/'}`,
+ ) &&
+ !isAbsolute(withinProject)
+ ) {
+ throw new Error(
+ 'Keep provisioning profiles outside the repository.',
+ );
+ }
+ profile = readProfile(profilePath);
+ identity = selectIdentity(
+ profile,
+ step('Checking local signing identities', 'security', [
+ 'find-identity',
+ '-v',
+ '-p',
+ 'codesigning',
+ ]),
+ mode,
+ );
+ }
+ mkdirSync(outputRoot, { recursive: true });
+ const working = mkdtempSync(join(outputRoot, '.building-'));
+ let installedProfile;
+ try {
+ if (profile) {
+ const installedPath = join(
+ home,
+ 'Library/Developer/Xcode/UserData/Provisioning Profiles',
+ `${profile.UUID}.mobileprovision`,
+ );
+ mkdirSync(dirname(installedPath), { recursive: true });
+ if (existsSync(installedPath)) {
+ if (
+ !readFileSync(installedPath).equals(
+ readFileSync(profilePath),
+ )
+ ) {
+ throw new Error(
+ 'A different provisioning profile already exists with the same UUID. Resolve it in Xcode before retrying.',
+ );
+ }
+ } else {
+ copyFileSync(profilePath, installedPath);
+ installedProfile = installedPath;
+ }
+ }
+ step('Generating the Xcode project', 'xcodegen', [
+ 'generate',
+ '--spec',
+ join(project, 'project.yml'),
+ ]);
+ step(
+ 'Building the iOS Rust library',
+ cargo,
+ [
+ 'build',
+ '--manifest-path',
+ join(directory, 'src-tauri/Cargo.toml'),
+ '--lib',
+ '--locked',
+ '--target',
+ target,
+ '--features',
+ 'custom-protocol',
+ ...(configuration === 'release' ? ['--release'] : []),
+ ],
+ { inherit: true },
+ );
+ const library = join(
+ targetDirectory,
+ target,
+ configuration,
+ 'libkanbangit_native.a',
+ );
+ if (!existsSync(library)) {
+ throw new Error(
+ `The iOS Rust library was not produced: ${library}`,
+ );
+ }
+ buildEnv.KANBANGIT_IOS_LIBRARY = library;
+ const archive = join(working, 'KanbanGit.xcarchive');
+ step(
+ 'Archiving locally without Apple provisioning updates',
+ 'xcodebuild',
+ [
+ '-project',
+ join(project, 'kanbangit-native.xcodeproj'),
+ '-scheme',
+ 'kanbangit-native_iOS',
+ '-configuration',
+ configuration,
+ '-sdk',
+ sdk,
+ '-destination',
+ `generic/platform=${simulator ? 'iOS Simulator' : 'iOS'}`,
+ '-archivePath',
+ archive,
+ '-derivedDataPath',
+ join(targetDirectory, `ios-derived-${mode}`),
+ 'archive',
+ `MARKETING_VERSION=${version}`,
+ `CURRENT_PROJECT_VERSION=${buildNumber}`,
+ `DEVELOPMENT_TEAM=${appleTeamId}`,
+ `PRODUCT_BUNDLE_IDENTIFIER=${bundleIdentifier}`,
+ 'CODE_SIGN_STYLE=Manual',
+ 'ARCHS=arm64',
+ ...(simulator
+ ? ['CODE_SIGNING_ALLOWED=NO', 'CODE_SIGNING_REQUIRED=NO']
+ : [
+ `CODE_SIGN_IDENTITY=${identity}`,
+ `PROVISIONING_PROFILE_SPECIFIER=${profile.UUID}`,
+ ]),
+ ],
+ { inherit: true },
+ );
+ function verifyApp(app) {
+ const info = parsePlist(readFileSync(join(app, 'Info.plist')));
+ if (
+ info.CFBundleIdentifier !== bundleIdentifier ||
+ info.CFBundleShortVersionString !== version ||
+ info.CFBundleVersion !== buildNumber
+ ) {
+ throw new Error(
+ 'The built app has an unexpected identifier, version or build number.',
+ );
+ }
+ if (!simulator) {
+ step('Verifying the Apple signature and team', 'codesign', [
+ '--verify',
+ '--deep',
+ '--strict',
+ '-R',
+ `=anchor apple generic and identifier "${bundleIdentifier}" and certificate leaf[subject.OU] = "${appleTeamId}" and certificate leaf = H"${identity}" and entitlement["application-identifier"] = "${appleTeamId}.${bundleIdentifier}" and entitlement["com.apple.developer.team-identifier"] = "${appleTeamId}" and ${mode === 'testflight' ? '! ' : ''}entitlement["get-task-allow"] exists${mode === 'testflight' ? ' and entitlement["beta-reports-active"] exists' : ''}`,
+ app,
+ ]);
+ if (
+ readProfile(join(app, 'embedded.mobileprovision')).UUID !==
+ profile.UUID
+ ) {
+ throw new Error(
+ 'The app was signed with an unexpected provisioning profile.',
+ );
+ }
+ }
+ }
+ verifyApp(join(archive, 'Products/Applications/KanbanGit.app'));
+ if (!simulator) {
+ const exportOptions = join(working, 'ExportOptions.plist');
+ writeFileSync(
+ exportOptions,
+ JSON.stringify({
+ method:
+ mode === 'testflight'
+ ? 'app-store-connect'
+ : 'debugging',
+ destination: 'export',
+ signingStyle: 'manual',
+ teamID: appleTeamId,
+ signingCertificate: identity,
+ provisioningProfiles: { [bundleIdentifier]: profile.UUID },
+ manageAppVersionAndBuildNumber: false,
+ }),
+ );
+ step('Preparing local export options', 'plutil', [
+ '-convert',
+ 'xml1',
+ exportOptions,
+ ]);
+ step(
+ 'Exporting the signed IPA locally (no upload)',
+ 'xcodebuild',
+ [
+ '-exportArchive',
+ '-archivePath',
+ archive,
+ '-exportPath',
+ join(working, 'export'),
+ '-exportOptionsPlist',
+ exportOptions,
+ ],
+ { inherit: true },
+ );
+ const ipa = join(working, 'export/KanbanGit.ipa');
+ if (!existsSync(ipa)) {
+ throw new Error('Xcode did not produce the signed IPA.');
+ }
+ const extracted = join(working, 'verify');
+ step('Verifying the exported IPA contents', 'ditto', [
+ '-x',
+ '-k',
+ ipa,
+ extracted,
+ ]);
+ verifyApp(join(extracted, 'Payload/KanbanGit.app'));
+ rmSync(extracted, { recursive: true, force: true });
+ writeFileSync(
+ `${ipa}.sha256`,
+ `${createHash('sha256').update(readFileSync(ipa)).digest('hex')} KanbanGit.ipa\n`,
+ );
+ }
+ if (mode !== 'testflight') {
+ rmSync(destination, { recursive: true, force: true });
+ }
+ renameSync(working, destination);
+ log(`Verified local ${mode} build: ${destination}`);
+ return destination;
+ } catch (error) {
+ log(`Build did not complete. Diagnostic output remains in ${working}`);
+ throw error;
+ } finally {
+ if (installedProfile) {
+ rmSync(installedProfile);
+ }
+ }
+}
+
+if (
+ process.argv[1] &&
+ import.meta.url === pathToFileURL(process.argv[1]).href
+) {
+ try {
+ const { values } = parseArgs({
+ options: { mode: { type: 'string', default: 'testflight' } },
+ });
+ buildIos({ mode: values.mode });
+ } catch (error) {
+ console.error(error.message);
+ process.exitCode = 1;
+ }
+}
diff --git a/native/package.json b/native/package.json
index d273e5e..ba2a080 100644
--- a/native/package.json
+++ b/native/package.json
@@ -14,8 +14,9 @@
"build:android": "tauri android build --debug --apk --target aarch64",
"smoke:macos": "cargo test --manifest-path src-tauri/Cargo.toml --locked --features smoke-test,custom-protocol --test macos_smoke",
"dev:ios": "tauri ios dev",
- "build:ios:simulator": "rm -rf src-tauri/gen/apple/build/arm64-sim/KanbanGit.app && tauri ios build --debug --target aarch64-sim --no-sign --ci",
- "build:ios:device": "tauri ios build --debug --target aarch64 --export-method debugging --archive-only",
+ "build:ios:simulator": "node build-ios.mjs --mode simulator",
+ "build:ios:device": "node build-ios.mjs --mode development",
+ "release:ios": "node build-ios.mjs --mode testflight",
"open:ios": "tauri ios dev --open --no-dev-server",
"prepare:ios": "xcodegen generate --spec src-tauri/gen/apple/project.yml",
"test:ios": "xcodebuild -project src-tauri/gen/apple/kanbangit-native.xcodeproj -scheme kanbangit-native_iOS -configuration debug -destination \"platform=iOS Simulator,id=${IOS_SIMULATOR_ID:?Set IOS_SIMULATOR_ID to an installed simulator UUID}\" -parallel-testing-enabled NO CODE_SIGNING_ALLOWED=NO test",
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png
index 3ba6c85..a17fcb8 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@1x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png
index e3f34d5..3305487 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x-1.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png
index e3f34d5..3305487 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png
index 8bcff76..62c3ed2 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-20x20@3x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png
index 1ed85c0..33334e0 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@1x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png
index b9cd314..e73794d 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x-1.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png
index b9cd314..e73794d 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png
index 6b56941..8a3e48c 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-29x29@3x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png
index e3f34d5..3305487 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@1x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png
index 51b1c2a..bca916d 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x-1.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png
index 51b1c2a..bca916d 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png
index 30decc9..a9a36ff 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-40x40@3x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png
index beb2f50..bf7966e 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-512@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png
index 30decc9..4d76835 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png
index cb0c0b2..c75d204 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-60x60@3x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png
index cdd488d..a2c30be 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@1x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png
index 56e188e..b993207 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-76x76@2x.png differ
diff --git a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png
index 283715a..89e6d03 100644
Binary files a/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png and b/native/src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5x83.5@2x.png differ
diff --git a/native/src-tauri/gen/apple/Tests/WorkspaceTests.swift b/native/src-tauri/gen/apple/Tests/WorkspaceTests.swift
index 4ee2972..4eb8e70 100644
--- a/native/src-tauri/gen/apple/Tests/WorkspaceTests.swift
+++ b/native/src-tauri/gen/apple/Tests/WorkspaceTests.swift
@@ -40,7 +40,7 @@ final class WorkspaceTests: XCTestCase {
controller.open(url)
let webview = try XCTUnwrap(controller.workspace)
webview.stopLoading()
- webview.loadHTMLString("
", baseURL: url)
+ webview.loadSimulatedRequest(URLRequest(url: url), responseHTML: "")
var loaded = false
for _ in 0..<50 {
if let value = try? await webview.evaluateJavaScript("document.getElementById('draft')?.value"),
@@ -57,10 +57,12 @@ final class WorkspaceTests: XCTestCase {
XCTAssertTrue(webview.canGoBack)
XCTAssertTrue(toolbar.items?.first?.isEnabled == true)
webview.goBack()
+ var returned = false
for _ in 0..<20 {
- if let hash = try? await webview.evaluateJavaScript("location.hash"), hash as? String == "" { break }
+ if let hash = try? await webview.evaluateJavaScript("location.hash"), hash as? String == "" { returned = true; break }
try await Task.sleep(for: .milliseconds(50))
}
+ XCTAssertTrue(returned)
let draft = try await webview.evaluateJavaScript("document.getElementById('draft').value")
XCTAssertEqual(draft as? String, "unsaved")
}
diff --git a/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj
index ec3205a..7d6c1ee 100644
--- a/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj
+++ b/native/src-tauri/gen/apple/kanbangit-native.xcodeproj/project.pbxproj
@@ -49,7 +49,9 @@
47C531812A31207F97A89C5F /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; };
4C76059D31C50B198779A5F7 /* en.lproj */ = {isa = PBXFileReference; path = en.lproj; sourceTree = ""; };
554AB059B4228085924F4959 /* WorkspacePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WorkspacePolicy.swift; sourceTree = ""; };
+ 5F71F06AB639140AB9714425 /* metadata.rs */ = {isa = PBXFileReference; path = metadata.rs; sourceTree = ""; };
63685B1189795CAEA85F7AB7 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
+ 6610D376CF901AC1733D394E /* updates.rs */ = {isa = PBXFileReference; path = updates.rs; sourceTree = ""; };
7440C7F0426E6B3977DD1D94 /* main.rs */ = {isa = PBXFileReference; path = main.rs; sourceTree = ""; };
8243D0C3821DAC731FECF105 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
91991DFDB0C064B06DF854ED /* libapp.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libapp.a; sourceTree = ""; };
@@ -146,7 +148,9 @@
2B96972F58CC158AB3DFF94A /* lib.rs */,
AEBAD9ABE758897C50BC1F2E /* locale.rs */,
7440C7F0426E6B3977DD1D94 /* main.rs */,
+ 5F71F06AB639140AB9714425 /* metadata.rs */,
E8BA104C81071623E1FF367D /* server.rs */,
+ 6610D376CF901AC1733D394E /* updates.rs */,
);
name = src;
path = ../../src;
@@ -269,12 +273,18 @@
LastUpgradeCheck = 1430;
TargetAttributes = {
721CF221CEDDC28FF2C5EBD5 = {
- DevelopmentTeam = "";
- ProvisioningStyle = Automatic;
+ DevelopmentTeam = ZZX2KZGTHU;
+ ProvisioningStyle = Manual;
};
988CFF304CB81C8F3A4D6612 = {
+ DevelopmentTeam = ZZX2KZGTHU;
+ ProvisioningStyle = Manual;
TestTargetID = 721CF221CEDDC28FF2C5EBD5;
};
+ CBC4561F8018D37DD49A0598 = {
+ DevelopmentTeam = ZZX2KZGTHU;
+ ProvisioningStyle = Manual;
+ };
};
};
buildConfigurationList = A9CA551DD3F3EBCCF8C41CC1 /* Build configuration list for PBXProject "kanbangit-native" */;
@@ -332,7 +342,7 @@
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
- shellScript = "export PATH=\"$(dirname \"$(rustup which cargo)\"):$PATH\"\nexport RUSTC=\"$(rustup which rustc)\"\ncd \"$SRCROOT/../../..\"\n./node_modules/.bin/tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}\n";
+ shellScript = "set -eu\nif [ -n \"${KANBANGIT_IOS_LIBRARY:-}\" ]; then\n mkdir -p \"$SRCROOT/Externals/arm64/$CONFIGURATION\"\n cp \"$KANBANGIT_IOS_LIBRARY\" \"$SRCROOT/Externals/arm64/$CONFIGURATION/libapp.a\"\n exit 0\nfi\nexport PATH=\"$(dirname \"$(rustup which cargo)\"):$PATH\"\nexport RUSTC=\"$(rustup which rustc)\"\ncd \"$SRCROOT/../../..\"\n./node_modules/.bin/tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths \"${FRAMEWORK_SEARCH_PATHS:?}\" --header-search-paths \"${HEADER_SEARCH_PATHS:?}\" --gcc-preprocessor-definitions \"${GCC_PREPROCESSOR_DEFINITIONS:-}\" --configuration ${CONFIGURATION:?} ${FORCE_COLOR:-} ${ARCHS:?}\n";
};
/* End PBXShellScriptBuildPhase section */
@@ -404,9 +414,10 @@
);
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "kanbangit-native_iOS/kanbangit-native_iOS.entitlements";
- CODE_SIGN_IDENTITY = "iPhone Developer";
- CODE_SIGN_STYLE = Automatic;
- DEVELOPMENT_TEAM = "";
+ CODE_SIGN_IDENTITY = "Apple Distribution";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = ZZX2KZGTHU;
ENABLE_BITCODE = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
@@ -421,9 +432,10 @@
);
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
- PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.desktop;
+ MARKETING_VERSION = 0.2.1;
+ PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.ios;
PRODUCT_MODULE_NAME = KanbanGitNative;
- PRODUCT_NAME = "KanbanGit";
+ PRODUCT_NAME = KanbanGit;
SDKROOT = iphoneos;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -498,13 +510,15 @@
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Manual;
+ DEVELOPMENT_TEAM = ZZX2KZGTHU;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.uitests;
+ PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.ios.uitests;
SDKROOT = iphoneos;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -577,9 +591,10 @@
);
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CODE_SIGN_ENTITLEMENTS = "kanbangit-native_iOS/kanbangit-native_iOS.entitlements";
- CODE_SIGN_IDENTITY = "iPhone Developer";
- CODE_SIGN_STYLE = Automatic;
- DEVELOPMENT_TEAM = "";
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Manual;
+ CURRENT_PROJECT_VERSION = 1;
+ DEVELOPMENT_TEAM = ZZX2KZGTHU;
ENABLE_BITCODE = NO;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
"EXCLUDED_ARCHS[sdk=iphoneos*]" = x86_64;
@@ -594,9 +609,10 @@
);
"LIBRARY_SEARCH_PATHS[arch=arm64]" = "$(inherited) $(PROJECT_DIR)/Externals/arm64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
"LIBRARY_SEARCH_PATHS[arch=x86_64]" = "$(inherited) $(PROJECT_DIR)/Externals/x86_64/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/$(PLATFORM_NAME) $(DEVELOPER_DIR)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-5.0/$(PLATFORM_NAME)";
- PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.desktop;
+ MARKETING_VERSION = 0.2.1;
+ PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.ios;
PRODUCT_MODULE_NAME = KanbanGitNative;
- PRODUCT_NAME = "KanbanGit";
+ PRODUCT_NAME = KanbanGit;
SDKROOT = iphoneos;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -608,13 +624,15 @@
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Manual;
+ DEVELOPMENT_TEAM = ZZX2KZGTHU;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.uitests;
+ PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.ios.uitests;
SDKROOT = iphoneos;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -626,13 +644,15 @@
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Manual;
+ DEVELOPMENT_TEAM = ZZX2KZGTHU;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.tests;
+ PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.ios.tests;
SDKROOT = iphoneos;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -643,13 +663,15 @@
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Manual;
+ DEVELOPMENT_TEAM = ZZX2KZGTHU;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
- PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.tests;
+ PRODUCT_BUNDLE_IDENTIFIER = de.kanbangit.ios.tests;
SDKROOT = iphoneos;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
diff --git a/native/src-tauri/gen/apple/kanbangit-native_iOS/Info.plist b/native/src-tauri/gen/apple/kanbangit-native_iOS/Info.plist
index d20aab9..124cc20 100644
--- a/native/src-tauri/gen/apple/kanbangit-native_iOS/Info.plist
+++ b/native/src-tauri/gen/apple/kanbangit-native_iOS/Info.plist
@@ -15,9 +15,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 0.1.0
+ $(MARKETING_VERSION)
CFBundleVersion
- 0.1.0
+ $(CURRENT_PROJECT_VERSION)
LSRequiresIPhoneOS
NSCameraUsageDescription
@@ -45,4 +45,4 @@
UIInterfaceOrientationLandscapeRight
-
\ No newline at end of file
+
diff --git a/native/src-tauri/gen/apple/project.yml b/native/src-tauri/gen/apple/project.yml
index d2d3e82..4eadb84 100644
--- a/native/src-tauri/gen/apple/project.yml
+++ b/native/src-tauri/gen/apple/project.yml
@@ -1,6 +1,6 @@
name: kanbangit-native
options:
- bundleIdPrefix: de.kanbangit.desktop
+ bundleIdPrefix: de.kanbangit.ios
deploymentTarget:
iOS: 17.0
fileGroups: [../../src]
@@ -14,10 +14,17 @@ settingGroups:
SWIFT_VERSION: "5.0"
PRODUCT_MODULE_NAME: KanbanGitNative
TARGETED_DEVICE_FAMILY: "1,2"
- CODE_SIGN_STYLE: Automatic
- DEVELOPMENT_TEAM: ""
+ CODE_SIGN_STYLE: Manual
+ DEVELOPMENT_TEAM: ZZX2KZGTHU
ENABLE_USER_SCRIPT_SANDBOXING: false
- PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.desktop
+ PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.ios
+ MARKETING_VERSION: "0.2.1"
+ CURRENT_PROJECT_VERSION: "1"
+ configs:
+ debug:
+ CODE_SIGN_IDENTITY: Apple Development
+ release:
+ CODE_SIGN_IDENTITY: Apple Distribution
targetTemplates:
app:
type: application
@@ -55,8 +62,8 @@ targets:
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
- CFBundleShortVersionString: 0.1.0
- CFBundleVersion: "0.1.0"
+ CFBundleShortVersionString: $(MARKETING_VERSION)
+ CFBundleVersion: $(CURRENT_PROJECT_VERSION)
entitlements:
path: kanbangit-native_iOS/kanbangit-native_iOS.entitlements
scheme:
@@ -88,10 +95,16 @@ targets:
- sdk: WebKit.framework
preBuildScripts:
- script: |
+ set -eu
+ if [ -n "${KANBANGIT_IOS_LIBRARY:-}" ]; then
+ mkdir -p "$SRCROOT/Externals/arm64/$CONFIGURATION"
+ cp "$KANBANGIT_IOS_LIBRARY" "$SRCROOT/Externals/arm64/$CONFIGURATION/libapp.a"
+ exit 0
+ fi
export PATH="$(dirname "$(rustup which cargo)"):$PATH"
export RUSTC="$(rustup which rustc)"
cd "$SRCROOT/../../.."
- ./node_modules/.bin/tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}
+ ./node_modules/.bin/tauri ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR:-} ${ARCHS:?}
name: Build Rust Code
basedOnDependencyAnalysis: false
outputFiles:
@@ -108,7 +121,9 @@ targets:
settings:
base:
GENERATE_INFOPLIST_FILE: YES
- PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.tests
+ PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.ios.tests
+ DEVELOPMENT_TEAM: ZZX2KZGTHU
+ CODE_SIGN_STYLE: Manual
SWIFT_VERSION: "5.0"
WorkspaceUITests:
type: bundle.ui-testing
@@ -119,6 +134,8 @@ targets:
settings:
base:
GENERATE_INFOPLIST_FILE: YES
- PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.uitests
+ PRODUCT_BUNDLE_IDENTIFIER: de.kanbangit.ios.uitests
+ DEVELOPMENT_TEAM: ZZX2KZGTHU
+ CODE_SIGN_STYLE: Manual
SWIFT_VERSION: "5.0"
TEST_TARGET_NAME: kanbangit-native_iOS
diff --git a/native/src-tauri/tauri.ios.conf.json b/native/src-tauri/tauri.ios.conf.json
index 6722463..9721cb6 100644
--- a/native/src-tauri/tauri.ios.conf.json
+++ b/native/src-tauri/tauri.ios.conf.json
@@ -1,6 +1,11 @@
{
"$schema": "https://schema.tauri.app/config/2",
+ "identifier": "de.kanbangit.ios",
"bundle": {
- "iOS": { "minimumSystemVersion": "17.0" }
+ "iOS": {
+ "minimumSystemVersion": "17.0",
+ "developmentTeam": "ZZX2KZGTHU",
+ "bundleVersion": "1"
+ }
}
}
diff --git a/native/tests/ios-build.test.js b/native/tests/ios-build.test.js
new file mode 100644
index 0000000..7462bd5
--- /dev/null
+++ b/native/tests/ios-build.test.js
@@ -0,0 +1,534 @@
+import assert from 'node:assert/strict';
+import { Buffer } from 'node:buffer';
+import { createHash } from 'node:crypto';
+import {
+ existsSync,
+ mkdirSync,
+ mkdtempSync,
+ readFileSync,
+ rmSync,
+ writeFileSync,
+} from 'node:fs';
+import { tmpdir } from 'node:os';
+import { dirname, join } from 'node:path';
+import test from 'node:test';
+import { fileURLToPath } from 'node:url';
+import {
+ appleTeamId,
+ buildIos,
+ bundleIdentifier,
+ selectIdentity,
+ validateProfile,
+} from '../build-ios.mjs';
+
+const certificate = Buffer.from('synthetic test certificate');
+const fingerprint = createHash('sha1')
+ .update(certificate)
+ .digest('hex')
+ .toUpperCase();
+
+function profileFor(mode = 'testflight') {
+ return {
+ UUID: '00000000-1111-2222-3333-444444444444',
+ TeamIdentifier: [appleTeamId],
+ Platform: ['iOS'],
+ ExpirationDate: '2099-01-01T00:00:00Z',
+ DeveloperCertificates: [certificate.toString('base64')],
+ ...(mode === 'development'
+ ? { ProvisionedDevices: ['test-device'] }
+ : {}),
+ Entitlements: {
+ 'application-identifier': `${appleTeamId}.${bundleIdentifier}`,
+ 'com.apple.developer.team-identifier': appleTeamId,
+ 'get-task-allow': mode === 'development',
+ 'beta-reports-active': mode === 'testflight',
+ },
+ };
+}
+
+function fixture(t, mode = 'testflight', overrides = {}) {
+ const root = mkdtempSync(join(tmpdir(), 'kanbangit-ios-test-'));
+ t.after(() => rmSync(root, { recursive: true, force: true }));
+ const directory = join(root, 'repo/native');
+ const profilePath = join(root, 'profile.mobileprovision');
+ const config = {
+ productName: 'KanbanGit',
+ identifier: 'de.kanbangit.desktop',
+ version: '1.2.3',
+ };
+ const ios = {
+ identifier: bundleIdentifier,
+ bundle: {
+ iOS: {
+ developmentTeam: appleTeamId,
+ bundleVersion: '7',
+ minimumSystemVersion: '17.0',
+ },
+ },
+ };
+ mkdirSync(join(directory, 'src-tauri'), { recursive: true });
+ writeFileSync(
+ join(directory, 'src-tauri/tauri.conf.json'),
+ JSON.stringify(config),
+ );
+ writeFileSync(
+ join(directory, 'src-tauri/tauri.ios.conf.json'),
+ JSON.stringify(ios),
+ );
+ writeFileSync(profilePath, 'external provisioning profile');
+ const profile = profileFor(mode);
+ const calls = [];
+ const destination = join(directory, `src-tauri/target/ios-${mode}/1.2.3-7`);
+ const home = join(root, 'home');
+ const installedProfile = join(
+ home,
+ `Library/Developer/Xcode/UserData/Provisioning Profiles/${profile.UUID}.mobileprovision`,
+ );
+ const info = {
+ CFBundleIdentifier: bundleIdentifier,
+ CFBundleShortVersionString: '1.2.3',
+ CFBundleVersion: '7',
+ };
+ function app(path) {
+ mkdirSync(path, { recursive: true });
+ writeFileSync(
+ join(path, 'Info.plist'),
+ JSON.stringify({ ...info, ...overrides.info }),
+ );
+ }
+ const options = {
+ directory,
+ home,
+ mode,
+ platform: 'darwin',
+ log() {},
+ env: { PATH: '/test/bin', IOS_PROVISIONING_PROFILE: profilePath },
+ run(command, args, settings) {
+ calls.push({ command, args, settings });
+ if (overrides.failStep === settings.step) {
+ throw new Error('Simulated build failure');
+ }
+ if (command === 'python3') {
+ return settings.input.toString();
+ }
+ if (command === 'security') {
+ if (args[0] === 'cms') {
+ const embedded = args
+ .at(-1)
+ .endsWith('embedded.mobileprovision');
+ return JSON.stringify(
+ embedded && overrides.embeddedProfile
+ ? overrides.embeddedProfile
+ : profile,
+ );
+ }
+ return `1) ${fingerprint} "Apple ${mode === 'development' ? 'Development' : 'Distribution'}: Example (${appleTeamId})"`;
+ }
+ if (command === 'rustup') {
+ if (args[0] === 'target') {
+ return (
+ overrides.targets ??
+ 'aarch64-apple-ios\naarch64-apple-ios-sim'
+ );
+ }
+ return `/permanent/toolchain/bin/${args[1]}`;
+ }
+ if (command === '/permanent/toolchain/bin/cargo') {
+ const library = join(
+ directory,
+ 'src-tauri/target',
+ args[args.indexOf('--target') + 1],
+ args.includes('--release') ? 'release' : 'debug',
+ 'libkanbangit_native.a',
+ );
+ mkdirSync(dirname(library), { recursive: true });
+ writeFileSync(library, 'compiled library');
+ }
+ if (command === 'xcodebuild' && args.includes('archive')) {
+ assert.ok(mode === 'simulator' || existsSync(installedProfile));
+ app(
+ join(
+ args[args.indexOf('-archivePath') + 1],
+ 'Products/Applications/KanbanGit.app',
+ ),
+ );
+ }
+ if (command === 'xcodebuild' && args.includes('-exportArchive')) {
+ const output = args[args.indexOf('-exportPath') + 1];
+ mkdirSync(output, { recursive: true });
+ if (!overrides.missingIpa) {
+ writeFileSync(
+ join(output, 'KanbanGit.ipa'),
+ 'signed ipa bytes',
+ );
+ }
+ }
+ if (command === 'ditto') {
+ app(join(args.at(-1), 'Payload/KanbanGit.app'));
+ }
+ if (command === 'xcrun') {
+ return '/permanent/Xcode/SDK';
+ }
+ return '';
+ },
+ };
+ return {
+ options,
+ calls,
+ profile,
+ profilePath,
+ destination,
+ installedProfile,
+ ios,
+ };
+}
+
+test('TestFlight produces a verified local IPA with manual signing and no Apple mutation or upload flags', (t) => {
+ const f = fixture(t);
+ f.options.env.APPLE_API_KEY = 'must-not-reach-build';
+ f.options.env.IOS_CERTIFICATE = 'must-not-reach-build';
+ assert.equal(buildIos(f.options), f.destination);
+ const options = JSON.parse(
+ readFileSync(join(f.destination, 'ExportOptions.plist'), 'utf8'),
+ );
+ assert.equal(options.method, 'app-store-connect');
+ assert.equal(options.destination, 'export');
+ assert.equal(options.signingStyle, 'manual');
+ assert.equal(options.manageAppVersionAndBuildNumber, false);
+ assert.equal(options.signingCertificate, fingerprint);
+ assert.deepEqual(options.provisioningProfiles, {
+ [bundleIdentifier]: f.profile.UUID,
+ });
+ assert.equal(
+ readFileSync(
+ join(f.destination, 'export/KanbanGit.ipa.sha256'),
+ 'utf8',
+ ),
+ `${createHash('sha256').update('signed ipa bytes').digest('hex')} KanbanGit.ipa\n`,
+ );
+ assert.equal(
+ f.calls.filter(
+ (call) =>
+ call.command === 'codesign' && call.args.includes('--verify'),
+ ).length,
+ 2,
+ );
+ assert.equal(existsSync(f.installedProfile), false);
+ const verification = f.calls.find((call) => call.command === 'codesign');
+ const requirement = verification.args[verification.args.indexOf('-R') + 1];
+ assert.ok(requirement.includes('! entitlement["get-task-allow"] exists'));
+ assert.ok(
+ requirement.includes('entitlement["beta-reports-active"] exists'),
+ );
+ assert.ok(requirement.includes(`certificate leaf = H"${fingerprint}"`));
+ for (const call of f.calls) {
+ assert.equal(call.settings.env.APPLE_API_KEY, undefined);
+ assert.equal(call.settings.env.IOS_CERTIFICATE, undefined);
+ assert.ok(
+ !call.args.some((arg) =>
+ /allowProvisioning|authenticationKey|upload|downloadPlatform/.test(
+ arg,
+ ),
+ ),
+ );
+ }
+ const archive = f.calls.find((call) => call.args.includes('archive'));
+ assert.ok(archive.args.includes('CODE_SIGN_STYLE=Manual'));
+ assert.ok(
+ archive.args.includes(
+ `PROVISIONING_PROFILE_SPECIFIER=${f.profile.UUID}`,
+ ),
+ );
+ assert.ok(
+ archive.settings.env.KANBANGIT_IOS_LIBRARY.endsWith(
+ '/aarch64-apple-ios/release/libkanbangit_native.a',
+ ),
+ );
+});
+
+test('the simulator needs no certificate or profile and disables signing explicitly', (t) => {
+ const f = fixture(t, 'simulator');
+ delete f.options.env.IOS_PROVISIONING_PROFILE;
+ buildIos(f.options);
+ assert.ok(
+ f.calls.some((call) => call.args.includes('CODE_SIGNING_ALLOWED=NO')),
+ );
+ assert.ok(
+ f.calls.some((call) => call.args.includes('aarch64-apple-ios-sim')),
+ );
+ assert.ok(
+ !f.calls.some(
+ (call) =>
+ ['security', 'codesign'].includes(call.command) ||
+ call.args.includes('-exportArchive'),
+ ),
+ );
+});
+
+test('local device development uses a registered-device development profile', (t) => {
+ const f = fixture(t, 'development');
+ buildIos(f.options);
+ const options = JSON.parse(
+ readFileSync(join(f.destination, 'ExportOptions.plist'), 'utf8'),
+ );
+ assert.equal(options.method, 'debugging');
+ assert.equal(options.destination, 'export');
+});
+
+for (const [name, change, message] of [
+ [
+ 'different team',
+ (p) => {
+ p.TeamIdentifier = ['OLDTEAM'];
+ },
+ /explicitly match/,
+ ],
+ [
+ 'wildcard identifier',
+ (p) => {
+ p.Entitlements['application-identifier'] = `${appleTeamId}.*`;
+ },
+ /explicitly match/,
+ ],
+ [
+ 'macOS identifier',
+ (p) => {
+ p.Entitlements['application-identifier'] =
+ `${appleTeamId}.de.kanbangit.desktop`;
+ },
+ /explicitly match/,
+ ],
+ [
+ 'expired profile',
+ (p) => {
+ p.ExpirationDate = '2000-01-01';
+ },
+ /expired/,
+ ],
+ [
+ 'invalid expiry',
+ (p) => {
+ p.ExpirationDate = 'invalid';
+ },
+ /expired/,
+ ],
+ [
+ 'invalid UUID',
+ (p) => {
+ p.UUID = '../unexpected';
+ },
+ /invalid/,
+ ],
+ [
+ 'missing certificate',
+ (p) => {
+ p.DeveloperCertificates = [];
+ },
+ /invalid/,
+ ],
+ [
+ 'wrong platform',
+ (p) => {
+ p.Platform = ['OSX'];
+ },
+ /iOS profile/,
+ ],
+ [
+ 'Ad Hoc profile',
+ (p) => {
+ p.ProvisionedDevices = ['device'];
+ },
+ /App Store Connect/,
+ ],
+ [
+ 'enterprise profile',
+ (p) => {
+ p.ProvisionsAllDevices = true;
+ },
+ /App Store Connect/,
+ ],
+ [
+ 'development entitlement',
+ (p) => {
+ p.Entitlements['get-task-allow'] = true;
+ },
+ /App Store Connect/,
+ ],
+ [
+ 'missing beta entitlement',
+ (p) => {
+ delete p.Entitlements['beta-reports-active'];
+ },
+ /App Store Connect/,
+ ],
+]) {
+ test(`rejects ${name} before signing`, () => {
+ const profile = profileFor();
+ change(profile);
+ assert.throws(() => validateProfile(profile, 'testflight'), message);
+ });
+}
+
+test('a distribution profile cannot be used for device development', () => {
+ assert.throws(
+ () => validateProfile(profileFor(), 'development'),
+ /App Development/,
+ );
+});
+
+test('identity selection requires the profile certificate and a usable private key of the correct kind', () => {
+ const profile = profileFor();
+ for (const identities of [
+ '0 valid identities found',
+ `1) ${'A'.repeat(40)} "Apple Distribution: Other"`,
+ `1) ${fingerprint} "Developer ID Application: Example"`,
+ `1) ${fingerprint} "Apple Development: Example"`,
+ ]) {
+ assert.throws(
+ () => selectIdentity(profile, identities, 'testflight'),
+ /private key/,
+ );
+ }
+});
+
+for (const step of [
+ 'Generating the Xcode project',
+ 'Building the iOS Rust library',
+ 'Archiving locally without Apple provisioning updates',
+ 'Verifying the Apple signature and team',
+ 'Exporting the signed IPA locally (no upload)',
+]) {
+ test(`failure at ${step} does not publish a successful build and cleans its profile`, (t) => {
+ const f = fixture(t, 'testflight', { failStep: step });
+ assert.throws(() => buildIos(f.options), /Simulated build failure/);
+ assert.equal(existsSync(f.destination), false);
+ assert.equal(existsSync(f.installedProfile), false);
+ });
+}
+
+test('an existing identical local profile is preserved after the build', (t) => {
+ const f = fixture(t);
+ mkdirSync(dirname(f.installedProfile), { recursive: true });
+ writeFileSync(f.installedProfile, readFileSync(f.profilePath));
+ buildIos(f.options);
+ assert.equal(existsSync(f.installedProfile), true);
+});
+
+test('a conflicting local profile is never overwritten or deleted', (t) => {
+ const f = fixture(t);
+ mkdirSync(dirname(f.installedProfile), { recursive: true });
+ writeFileSync(f.installedProfile, 'user profile');
+ assert.throws(() => buildIos(f.options), /same UUID/);
+ assert.equal(readFileSync(f.installedProfile, 'utf8'), 'user profile');
+});
+
+test('missing toolchain targets fail before compilation without installing anything', (t) => {
+ const f = fixture(t, 'testflight', { targets: 'aarch64-apple-darwin' });
+ assert.throws(
+ () => buildIos(f.options),
+ /rustup target add aarch64-apple-ios/,
+ );
+ assert.ok(
+ !f.calls.some(
+ (call) => call.args.includes('build') || call.args.includes('add'),
+ ),
+ );
+});
+
+test('missing profile and ambient team overrides fail clearly', (t) => {
+ const f = fixture(t);
+ delete f.options.env.IOS_PROVISIONING_PROFILE;
+ assert.throws(() => buildIos(f.options), /IOS_PROVISIONING_PROFILE/);
+ f.options.env.APPLE_DEVELOPMENT_TEAM = 'OLDTEAM';
+ assert.throws(() => buildIos(f.options), /must be ZZX2KZGTHU/);
+ delete f.options.env.APPLE_DEVELOPMENT_TEAM;
+ f.options.env.TAURI_CONFIG = '{}';
+ assert.throws(() => buildIos(f.options), /Remove TAURI_CONFIG/);
+});
+
+for (const [name, overrides, message] of [
+ [
+ 'wrong app version',
+ { info: { CFBundleVersion: '8' } },
+ /unexpected identifier, version/,
+ ],
+ [
+ 'changed embedded profile',
+ {
+ embeddedProfile: {
+ ...profileFor(),
+ UUID: 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
+ },
+ },
+ /unexpected provisioning profile/,
+ ],
+ ['missing IPA', { missingIpa: true }, /did not produce/],
+]) {
+ test(`rejects ${name} in the build output`, (t) => {
+ const f = fixture(t, 'testflight', overrides);
+ assert.throws(() => buildIos(f.options), message);
+ assert.equal(existsSync(f.destination), false);
+ });
+}
+
+test('completed artifacts are not silently overwritten', (t) => {
+ const f = fixture(t);
+ buildIos(f.options);
+ assert.throws(() => buildIos(f.options), /already exists/);
+});
+
+for (const mode of ['simulator', 'development']) {
+ test(`${mode} can rebuild the same version and preserves the previous build on failure`, (t) => {
+ const overrides = {};
+ const f = fixture(t, mode, overrides);
+ buildIos(f.options);
+ const previousBuild = join(f.destination, 'previous-build');
+ writeFileSync(previousBuild, 'verified artifact');
+ overrides.failStep = 'Building the iOS Rust library';
+ assert.throws(() => buildIos(f.options), /Simulated build failure/);
+ assert.equal(readFileSync(previousBuild, 'utf8'), 'verified artifact');
+ delete overrides.failStep;
+ buildIos(f.options);
+ assert.equal(existsSync(previousBuild), false);
+ assert.ok(existsSync(join(f.destination, 'KanbanGit.xcarchive')));
+ assert.equal(existsSync(f.installedProfile), false);
+ });
+}
+
+test('every iOS icon has its declared dimensions and contains no alpha channel or transparency chunk', () => {
+ const assets = fileURLToPath(
+ new URL(
+ '../src-tauri/gen/apple/Assets.xcassets/AppIcon.appiconset/',
+ import.meta.url,
+ ),
+ );
+ const catalog = JSON.parse(
+ readFileSync(join(assets, 'Contents.json'), 'utf8'),
+ );
+ assert.ok(
+ catalog.images.some(
+ (icon) =>
+ icon.idiom === 'ios-marketing' && icon.size === '1024x1024',
+ ),
+ );
+ for (const icon of catalog.images) {
+ const png = readFileSync(join(assets, icon.filename));
+ assert.equal(png.subarray(0, 8).toString('hex'), '89504e470d0a1a0a');
+ const [width, height] = icon.size.split('x').map(Number);
+ const scale = Number.parseInt(icon.scale, 10);
+ assert.equal(png.readUInt32BE(16), width * scale, icon.filename);
+ assert.equal(png.readUInt32BE(20), height * scale, icon.filename);
+ assert.equal(png[25], 2, `${icon.filename} must use RGB without alpha`);
+ for (
+ let offset = 8;
+ offset < png.length;
+ offset += png.readUInt32BE(offset) + 12
+ ) {
+ assert.notEqual(
+ png.subarray(offset + 4, offset + 8).toString(),
+ 'tRNS',
+ icon.filename,
+ );
+ }
+ }
+});