refactor(version): derive Apple build numbers

This commit is contained in:
2026-07-28 11:27:45 +02:00
parent 2d7982bbb9
commit c6655da7db
17 changed files with 248 additions and 55 deletions

View File

@@ -145,6 +145,7 @@ jobs:
name: vnidrop-${{ steps.version.outputs.app }}-macos-dmg name: vnidrop-${{ steps.version.outputs.app }}-macos-dmg
path: | path: |
apple/dist/VniDrop-*.dmg apple/dist/VniDrop-*.dmg
apple/dist/VniDrop-*.build-info.json
apple/dist/appcast.xml apple/dist/appcast.xml
if-no-files-found: error if-no-files-found: error
retention-days: 14 retention-days: 14

1
.gitignore vendored
View File

@@ -25,3 +25,4 @@ config.override.mk
output/ output/
.screenshots .screenshots
apple/RELEASE-MACOS.md apple/RELEASE-MACOS.md
apple/Generated/*.xcconfig

View File

@@ -12,7 +12,7 @@ include $(ROOT)/make/release.mk
.PHONY: format test check check-rust audit-rust test-rust test-rust-all .PHONY: format test check check-rust audit-rust test-rust test-rust-all
.PHONY: test-rust-transfer test-rust-approval test-rust-lifecycle test-rust-output-sink .PHONY: test-rust-transfer test-rust-approval test-rust-lifecycle test-rust-output-sink
.PHONY: check-shared test-shared test-android-host check-android verify-android-libs build-android run-desktop .PHONY: check-shared test-shared test-android-host check-android verify-android-libs build-android run-desktop
.PHONY: apple-core apple-project open-apple-project open-apple build-apple-macos build-apple-ios check-apple .PHONY: apple-core apple-version-config apple-project open-apple-project open-apple build-apple-macos build-apple-ios check-apple
.PHONY: check-version check-release check-localization localization localization-migrate .PHONY: check-version check-release check-localization localization localization-migrate
.PHONY: check-docs run-docs check-diagnostics run-diagnostics diagnostics-db-local diagnostics-db-remote diagnostics-typegen deploy-diagnostics .PHONY: check-docs run-docs check-diagnostics run-diagnostics diagnostics-db-local diagnostics-db-remote diagnostics-typegen deploy-diagnostics
@@ -124,7 +124,10 @@ apple-core: ## Build the Rust XCFramework and generated Swift bindings.
@test "$(HOST_OS)" = macos || { printf 'Apple builds require macOS.\n' >&2; exit 1; } @test "$(HOST_OS)" = macos || { printf 'Apple builds require macOS.\n' >&2; exit 1; }
cd $(ROOT) && apple/scripts/build-core.sh $(APPLE_PROFILE) cd $(ROOT) && apple/scripts/build-core.sh $(APPLE_PROFILE)
apple-project: apple-core localization ## Generate the native Apple Xcode project. apple-version-config: ## Generate derived Store and Direct Apple build settings.
cd $(ROOT) && packaging/version/generate-apple-xcconfig.sh all
apple-project: apple-core localization apple-version-config ## Generate the native Apple Xcode project.
cd $(ROOT)/apple && $(XCODEGEN) generate cd $(ROOT)/apple && $(XCODEGEN) generate
open-apple-project: apple-project ## Generate and open the native Apple Xcode project. open-apple-project: apple-project ## Generate and open the native Apple Xcode project.

View File

@@ -40,6 +40,12 @@ make build-apple-ios # unsigned iOS simulator app
make check-apple # iOS simulator tests make check-apple # iOS simulator tests
``` ```
`make apple-project` also generates ignored Store and Direct version xcconfig
files. Their `CURRENT_PROJECT_VERSION` values come from the central version
resolver as UTC `YYYYMMDD.HHMM.SS` build identifiers. Regenerate the project
before creating another App Store archive so it receives a fresh build number;
direct DMG builds refresh their own value automatically.
### macOS shipping channels ### macOS shipping channels
The macOS app ships through two targets that build identical sources: The macOS app ships through two targets that build identical sources:

View File

@@ -1,6 +1,3 @@
// Product and build versions are shared by every platform.
#include "../version.properties"
// VniDrop development and CI builds are intentionally unsigned. // VniDrop development and CI builds are intentionally unsigned.
CODE_SIGNING_ALLOWED = NO CODE_SIGNING_ALLOWED = NO
CODE_SIGNING_REQUIRED = NO CODE_SIGNING_REQUIRED = NO

View File

@@ -1,9 +1,11 @@
# XcodeGen spec for the native SwiftUI VniDrop app (iOS/iPadOS/macOS). # XcodeGen spec for the native SwiftUI VniDrop app (iOS/iPadOS/macOS).
# Regenerate the project with: xcodegen generate (run from apple/) # Regenerate the project with: xcodegen generate (run from apple/)
# Requires two generated inputs first (both gitignored), before xcodegen: # Requires three generated inputs first (all gitignored), before xcodegen:
# - Rust core: apple/scripts/build-core.sh debug # - Rust core: apple/scripts/build-core.sh debug
# - Localization: (cd localization && bun run src/cli.ts generate) # - Localization: (cd localization && bun run src/cli.ts generate)
# -> VniDrop/Resources/Localizable.xcstrings, VniDrop/Generated/L10n.swift # -> VniDrop/Resources/Localizable.xcstrings, VniDrop/Generated/L10n.swift
# - Versions: packaging/version/generate-apple-xcconfig.sh all
# -> Generated/StoreVersion.xcconfig, Generated/DirectVersion.xcconfig
name: VniDrop name: VniDrop
options: options:
bundleIdPrefix: com.vnidrop bundleIdPrefix: com.vnidrop
@@ -50,10 +52,6 @@ packages:
targetTemplates: targetTemplates:
AppBase: AppBase:
type: application type: application
configFiles:
Debug: Signing.xcconfig
Release: Signing.xcconfig
Release-Direct: Signing.xcconfig
sources: sources:
- path: VniDrop - path: VniDrop
excludes: excludes:
@@ -66,7 +64,6 @@ targetTemplates:
PRODUCT_NAME: VniDrop PRODUCT_NAME: VniDrop
PRODUCT_BUNDLE_IDENTIFIER: com.vnidrop.app PRODUCT_BUNDLE_IDENTIFIER: com.vnidrop.app
MARKETING_VERSION: "$(PRODUCT_VERSION)" MARKETING_VERSION: "$(PRODUCT_VERSION)"
CURRENT_PROJECT_VERSION: "$(APPLE_BUILD_NUMBER)"
GENERATE_INFOPLIST_FILE: NO GENERATE_INFOPLIST_FILE: NO
INFOPLIST_FILE: VniDrop/Resources/Info.plist INFOPLIST_FILE: VniDrop/Resources/Info.plist
CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements
@@ -114,6 +111,10 @@ targets:
VniDrop: VniDrop:
templates: [AppBase] templates: [AppBase]
supportedDestinations: [iOS, macOS] supportedDestinations: [iOS, macOS]
configFiles:
Debug: Generated/StoreVersion.xcconfig
Release: Generated/StoreVersion.xcconfig
Release-Direct: Generated/StoreVersion.xcconfig
# Direct-download macOS target: Developer ID signed, notarized, ships in a .dmg # Direct-download macOS target: Developer ID signed, notarized, ships in a .dmg
# and self-updates via Sparkle. DIRECT_DISTRIBUTION gates all Sparkle code so the # and self-updates via Sparkle. DIRECT_DISTRIBUTION gates all Sparkle code so the
@@ -121,6 +122,10 @@ targets:
VniDropDirect: VniDropDirect:
templates: [AppBase] templates: [AppBase]
supportedDestinations: [macOS] supportedDestinations: [macOS]
configFiles:
Debug: Generated/DirectVersion.xcconfig
Release: Generated/DirectVersion.xcconfig
Release-Direct: Generated/DirectVersion.xcconfig
settings: settings:
base: base:
SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) DIRECT_DISTRIBUTION" SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) DIRECT_DISTRIBUTION"

View File

@@ -29,10 +29,13 @@ SCHEME="VniDropDirect"
CONFIG="Release-Direct" CONFIG="Release-Direct"
APP_NAME="VniDrop" APP_NAME="VniDrop"
VERSION_RESOLVER="$REPO_ROOT/packaging/version/resolve-version.sh" VERSION_RESOLVER="$REPO_ROOT/packaging/version/resolve-version.sh"
VERSION_CONFIG_GENERATOR="$REPO_ROOT/packaging/version/generate-apple-xcconfig.sh"
VERSION="$("$VERSION_RESOLVER" product)" VERSION="$("$VERSION_RESOLVER" product)"
BUILD_NUMBER="$("$VERSION_RESOLVER" apple-build)" export VNIDROP_BUILD_TIME_UTC="${VNIDROP_BUILD_TIME_UTC:-$(date -u +%Y%m%d%H%M%S)}"
BUILD_NUMBER="$("$VERSION_RESOLVER" apple-direct-build)"
"$VERSION_RESOLVER" verify >/dev/null "$VERSION_RESOLVER" verify >/dev/null
BUILD_METADATA="$DIST_DIR/$APP_NAME-$VERSION.build-info.json"
# --- Resolve signing identity ------------------------------------------------ # --- Resolve signing identity ------------------------------------------------
if [ -z "${DEVELOPER_ID_APP:-}" ]; then if [ -z "${DEVELOPER_ID_APP:-}" ]; then
@@ -57,6 +60,7 @@ echo " team: ${DEVELOPMENT_TEAM:-<unknown>}"
echo "==> Building Rust core (release)" echo "==> Building Rust core (release)"
CARGO_PROFILE_RELEASE_LTO=false "$SCRIPT_DIR/build-core.sh" release CARGO_PROFILE_RELEASE_LTO=false "$SCRIPT_DIR/build-core.sh" release
echo "==> Regenerating Xcode project" echo "==> Regenerating Xcode project"
"$VERSION_CONFIG_GENERATOR" all
( cd "$APPLE_DIR" && xcodegen generate >/dev/null ) ( cd "$APPLE_DIR" && xcodegen generate >/dev/null )
rm -rf "$BUILD_DIR" && mkdir -p "$BUILD_DIR" "$DIST_DIR" rm -rf "$BUILD_DIR" && mkdir -p "$BUILD_DIR" "$DIST_DIR"
@@ -88,6 +92,18 @@ xcodebuild -exportArchive \
-exportOptionsPlist "$EXPORT_OPTS" -exportOptionsPlist "$EXPORT_OPTS"
APP="$EXPORT_DIR/$APP_NAME.app" APP="$EXPORT_DIR/$APP_NAME.app"
[ -d "$APP" ] || { echo "error: export failed" >&2; exit 1; } [ -d "$APP" ] || { echo "error: export failed" >&2; exit 1; }
ACTUAL_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \
"$APP/Contents/Info.plist")"
ACTUAL_BUILD="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' \
"$APP/Contents/Info.plist")"
[ "$ACTUAL_VERSION" = "$VERSION" ] || {
echo "error: exported app version $ACTUAL_VERSION does not match $VERSION" >&2
exit 1
}
[ "$ACTUAL_BUILD" = "$BUILD_NUMBER" ] || {
echo "error: exported app build $ACTUAL_BUILD does not match $BUILD_NUMBER" >&2
exit 1
}
# --- Build the DMG ----------------------------------------------------------- # --- Build the DMG -----------------------------------------------------------
DMG="$DIST_DIR/$APP_NAME-$VERSION.dmg" DMG="$DIST_DIR/$APP_NAME-$VERSION.dmg"
@@ -133,7 +149,18 @@ else
fi fi
SIZE="$(stat -f%z "$DMG")" SIZE="$(stat -f%z "$DMG")"
jq -n \
--arg productVersion "$VERSION" \
--arg directBuildNumber "$BUILD_NUMBER" \
--arg artifact "$(basename "$DMG")" \
'{
productVersion: $productVersion,
directBuildNumber: $directBuildNumber,
distribution: "direct",
artifact: $artifact
}' > "$BUILD_METADATA"
echo "==> Done." echo "==> Done."
echo " dmg: $DMG" echo " dmg: $DMG"
echo " version: $VERSION" echo " version: $VERSION"
echo " build: $BUILD_NUMBER"
echo " size: $SIZE bytes" echo " size: $SIZE bytes"

View File

@@ -54,10 +54,6 @@ val androidVersionCode = canonicalInteger(
requiredVersionProperty("ANDROID_VERSION_CODE"), requiredVersionProperty("ANDROID_VERSION_CODE"),
1L..2_100_000_000L, 1L..2_100_000_000L,
).toInt() ).toInt()
val appleBuildNumber = requiredVersionProperty("APPLE_BUILD_NUMBER")
require(appleBuildNumber.matches(Regex("[1-9][0-9]*(\\.[0-9]+){0,2}"))) {
"APPLE_BUILD_NUMBER must contain one to three period-separated non-negative integers and start above zero"
}
val windowsVersionEpoch = canonicalInteger( val windowsVersionEpoch = canonicalInteger(
"WINDOWS_VERSION_EPOCH", "WINDOWS_VERSION_EPOCH",
requiredVersionProperty("WINDOWS_VERSION_EPOCH"), requiredVersionProperty("WINDOWS_VERSION_EPOCH"),
@@ -73,7 +69,6 @@ val windowsPackageVersion =
extra["vnidrop.productVersion"] = productVersion extra["vnidrop.productVersion"] = productVersion
extra["vnidrop.releaseChannel"] = releaseChannel extra["vnidrop.releaseChannel"] = releaseChannel
extra["vnidrop.androidVersionCode"] = androidVersionCode extra["vnidrop.androidVersionCode"] = androidVersionCode
extra["vnidrop.appleBuildNumber"] = appleBuildNumber
extra["vnidrop.windowsPackageVersion"] = windowsPackageVersion extra["vnidrop.windowsPackageVersion"] = windowsPackageVersion
tasks.register("verifyVersion") { tasks.register("verifyVersion") {
@@ -83,6 +78,5 @@ tasks.register("verifyVersion") {
inputs.property("productVersion", productVersion) inputs.property("productVersion", productVersion)
inputs.property("releaseChannel", releaseChannel) inputs.property("releaseChannel", releaseChannel)
inputs.property("androidVersionCode", androidVersionCode) inputs.property("androidVersionCode", androidVersionCode)
inputs.property("appleBuildNumber", appleBuildNumber)
inputs.property("windowsPackageVersion", windowsPackageVersion) inputs.property("windowsPackageVersion", windowsPackageVersion)
} }

View File

@@ -27,8 +27,9 @@ private workflow artifacts. Partner Center submission stays manual until the
first Microsoft Store release is certified. The Play release remains a draft first Microsoft Store release is certified. The Play release remains a draft
on a closed-testing track; this pipeline cannot publish to production. on a closed-testing track; this pipeline cannot publish to production.
To release, first update and merge `version.properties`, including monotonic To release, first update and merge `version.properties`, including a monotonic
Android and Apple build numbers. Then create and push the matching tag: Android version code. Apple Store and Direct build numbers are derived
independently at build time. Then create and push the matching tag:
```bash ```bash
git tag -s v0.2.0 -m "VniDrop 0.2.0" git tag -s v0.2.0 -m "VniDrop 0.2.0"

View File

@@ -45,7 +45,6 @@ verify_checksum_file() {
version="$("$resolver" product)" version="$("$resolver" product)"
android_code="$("$resolver" android-code)" android_code="$("$resolver" android-code)"
apple_build="$("$resolver" apple-build)"
windows_package="$("$resolver" windows-package)" windows_package="$("$resolver" windows-package)"
"$resolver" verify >/dev/null "$resolver" verify >/dev/null
[[ $source_tag == "v$version" ]] || { [[ $source_tag == "v$version" ]] || {
@@ -58,6 +57,7 @@ deb="$(find_single "$input_dir/deb" '*.deb' 'Debian package')"
rpm="$(find_single "$input_dir/rpm" '*.rpm' 'RPM package')" rpm="$(find_single "$input_dir/rpm" '*.rpm' 'RPM package')"
dmg="$(find_single "$input_dir/macos" '*.dmg' 'macOS DMG')" dmg="$(find_single "$input_dir/macos" '*.dmg' 'macOS DMG')"
appcast="$(find_single "$input_dir/macos" 'appcast.xml' 'Sparkle appcast')" appcast="$(find_single "$input_dir/macos" 'appcast.xml' 'Sparkle appcast')"
apple_metadata="$(find_single "$input_dir/macos" '*.build-info.json' 'direct macOS build metadata')"
play_apk="$(find_single "$input_dir/play" '*-play-universal.apk' 'Play-signed APK')" play_apk="$(find_single "$input_dir/play" '*-play-universal.apk' 'Play-signed APK')"
play_metadata="$(find_single "$input_dir/play" 'play-release.json' 'Play release metadata')" play_metadata="$(find_single "$input_dir/play" 'play-release.json' 'Play release metadata')"
msix="$(find_single "$input_dir/windows" '*.msix' 'Windows MSIX')" msix="$(find_single "$input_dir/windows" '*.msix' 'Windows MSIX')"
@@ -70,6 +70,14 @@ windows_metadata="$(find_single "$input_dir/windows" '*.build-info.json' 'Window
[[ $(basename "$play_apk") == "VniDrop-${version}-${android_code}-play-universal.apk" ]] [[ $(basename "$play_apk") == "VniDrop-${version}-${android_code}-play-universal.apk" ]]
[[ $(basename "$msix") == "VniDrop_${version}_x64.msix" ]] [[ $(basename "$msix") == "VniDrop_${version}_x64.msix" ]]
[[ $(basename "$msixupload") == "VniDrop_${version}_x64.msixupload" ]] [[ $(basename "$msixupload") == "VniDrop_${version}_x64.msixupload" ]]
[[ $(jq -r '.productVersion' "$apple_metadata") == "$version" ]]
[[ $(jq -r '.distribution' "$apple_metadata") == direct ]]
[[ $(jq -r '.artifact' "$apple_metadata") == "$(basename "$dmg")" ]]
apple_direct_build="$(jq -r '.directBuildNumber' "$apple_metadata")"
[[ $apple_direct_build =~ ^[1-9][0-9]*(\.[0-9]+){0,2}$ ]] || {
printf 'Invalid direct Apple build number: %s\n' "$apple_direct_build" >&2
exit 1
}
deb_checksum="$(find_single "$input_dir/deb" '*.sha256' 'Debian checksum')" deb_checksum="$(find_single "$input_dir/deb" '*.sha256' 'Debian checksum')"
rpm_checksum="$(find_single "$input_dir/rpm" '*.sha256' 'RPM checksum')" rpm_checksum="$(find_single "$input_dir/rpm" '*.sha256' 'RPM checksum')"
@@ -120,7 +128,7 @@ jq -n \
--arg tag "$source_tag" \ --arg tag "$source_tag" \
--arg commit "$source_commit" \ --arg commit "$source_commit" \
--arg androidVersionCode "$android_code" \ --arg androidVersionCode "$android_code" \
--arg appleBuildNumber "$apple_build" \ --arg appleDirectBuildNumber "$apple_direct_build" \
--arg windowsPackageVersion "$windows_package" \ --arg windowsPackageVersion "$windows_package" \
--arg windowsMsixUpload "$(basename "$msixupload")" \ --arg windowsMsixUpload "$(basename "$msixupload")" \
--arg windowsMsixUploadSha256 "$(sha256sum "$msixupload" | awk '{print $1}')" \ --arg windowsMsixUploadSha256 "$(sha256sum "$msixupload" | awk '{print $1}')" \
@@ -135,7 +143,7 @@ jq -n \
sourceCommit: $commit, sourceCommit: $commit,
platformVersions: { platformVersions: {
androidVersionCode: ($androidVersionCode | tonumber), androidVersionCode: ($androidVersionCode | tonumber),
appleBuildNumber: $appleBuildNumber, appleDirectBuildNumber: $appleDirectBuildNumber,
windowsPackageVersion: $windowsPackageVersion windowsPackageVersion: $windowsPackageVersion
}, },
play: { play: {

View File

@@ -19,6 +19,7 @@ mkdir -p \
version="$("$repo_root/packaging/version/resolve-version.sh" product)" version="$("$repo_root/packaging/version/resolve-version.sh" product)"
android_code="$("$repo_root/packaging/version/resolve-version.sh" android-code)" android_code="$("$repo_root/packaging/version/resolve-version.sh" android-code)"
windows_package="$("$repo_root/packaging/version/resolve-version.sh" windows-package)" windows_package="$("$repo_root/packaging/version/resolve-version.sh" windows-package)"
apple_direct_build=20260728.1432.17
printf 'deb\n' > "$input_dir/deb/vnidrop_${version}-1_amd64.deb" printf 'deb\n' > "$input_dir/deb/vnidrop_${version}-1_amd64.deb"
printf 'rpm\n' > "$input_dir/rpm/vnidrop-${version}-1.x86_64.rpm" printf 'rpm\n' > "$input_dir/rpm/vnidrop-${version}-1.x86_64.rpm"
@@ -28,6 +29,17 @@ printf 'apk\n' > "$input_dir/play/VniDrop-${version}-${android_code}-play-univer
printf 'msix\n' > "$input_dir/windows/VniDrop_${version}_x64.msix" printf 'msix\n' > "$input_dir/windows/VniDrop_${version}_x64.msix"
printf 'msixupload\n' > "$input_dir/windows/VniDrop_${version}_x64.msixupload" printf 'msixupload\n' > "$input_dir/windows/VniDrop_${version}_x64.msixupload"
jq -n \
--arg productVersion "$version" \
--arg directBuildNumber "$apple_direct_build" \
--arg artifact "VniDrop-${version}.dmg" \
'{
productVersion: $productVersion,
directBuildNumber: $directBuildNumber,
distribution: "direct",
artifact: $artifact
}' > "$input_dir/macos/VniDrop-${version}.build-info.json"
jq -n \ jq -n \
--arg releaseName "$version" \ --arg releaseName "$version" \
--argjson versionCode "$android_code" \ --argjson versionCode "$android_code" \
@@ -94,6 +106,8 @@ done < <(find "$output_dir" -maxdepth 1 -type f -print | sort)
[[ ${actual_public_files[*]} == "${expected_public_files[*]}" ]] [[ ${actual_public_files[*]} == "${expected_public_files[*]}" ]]
[[ $(jq -r '.productVersion' "$output_dir/release-manifest.json") == "$version" ]] [[ $(jq -r '.productVersion' "$output_dir/release-manifest.json") == "$version" ]]
[[ $(jq -r '.platformVersions.appleDirectBuildNumber' \
"$output_dir/release-manifest.json") == "$apple_direct_build" ]]
[[ $(jq -r '.play.status' "$output_dir/release-manifest.json") == draft ]] [[ $(jq -r '.play.status' "$output_dir/release-manifest.json") == draft ]]
[[ $(jq -r '.windowsStore.publicReleaseAsset' "$output_dir/release-manifest.json") == false ]] [[ $(jq -r '.windowsStore.publicReleaseAsset' "$output_dir/release-manifest.json") == false ]]
( (

View File

@@ -1,11 +1,12 @@
# Application versioning # Application versioning
`version.properties` at the repository root is the single source of truth for `version.properties` at the repository root is the single source of truth for
the VniDrop application version. Platform projects and release workflows read the VniDrop product version and persistent store counters. Platform projects
that file rather than accepting independent version overrides. and release workflows use the version resolver rather than accepting
independent version overrides.
Keep it as plain `KEY=VALUE` assignments: the same file is parsed by shell, Keep it as plain `KEY=VALUE` assignments: the same file is parsed by shell,
PowerShell, Gradle, Rust, and Xcode. PowerShell, Gradle, and Rust. Xcode receives resolver-generated xcconfig files.
The product uses numeric semantic versions. While the app is in beta, feature The product uses numeric semantic versions. While the app is in beta, feature
releases increment the minor component (`0.2.0`, `0.3.0`) and fixes increment releases increment the minor component (`0.2.0`, `0.3.0`) and fixes increment
@@ -15,8 +16,9 @@ the patch component (`0.2.1`). Release channels belong in
| Platform | Product version | Platform build/package version | | Platform | Product version | Platform build/package version |
| --- | --- | --- | | --- | --- | --- |
| Android | `PRODUCT_VERSION` | `ANDROID_VERSION_CODE` | | Android | `PRODUCT_VERSION` | `ANDROID_VERSION_CODE` |
| Apple | `PRODUCT_VERSION` | `APPLE_BUILD_NUMBER` | | Apple Store | `PRODUCT_VERSION` | Derived UTC `YYYYMMDD.HHMM.SS` |
| Linux and direct macOS | `PRODUCT_VERSION` | Native package revision | | Direct macOS | `PRODUCT_VERSION` | Independently derived UTC `YYYYMMDD.HHMM.SS` |
| Linux | `PRODUCT_VERSION` | Native package revision |
| Rust handshake | `PRODUCT_VERSION` | Rust crate version remains independent | | Rust handshake | `PRODUCT_VERSION` | Rust crate version remains independent |
| Microsoft Store | `PRODUCT_VERSION` in the app | Derived MSIX dot-quad | | Microsoft Store | `PRODUCT_VERSION` in the app | Derived MSIX dot-quad |
@@ -30,9 +32,23 @@ the Store. Its version is:
With epoch `1`, product `0.2.0` maps to MSIX `1.2.0.0`, while product `1.0.0` With epoch `1`, product `0.2.0` maps to MSIX `1.2.0.0`, while product `1.0.0`
maps to `2.0.0.0`. Do not change the epoch after publishing. maps to `2.0.0.0`. Do not change the epoch after publishing.
Every Android or Apple upload must increment its platform build number. Every Every Android upload must increment `ANDROID_VERSION_CODE`. Apple build numbers
changed Windows Store package must increment the product version because the are derived at build time by `apple-store-build` and `apple-direct-build`; they
Store-reserved fourth component cannot carry a rebuild number. are kept as separate resolver outputs so App Store and Sparkle releases do not
consume each other's cadence. Every changed Windows Store package must
increment the product version because the Store-reserved fourth component
cannot carry a rebuild number.
Apple projects read generated build settings rather than `version.properties`
directly:
```bash
packaging/version/generate-apple-xcconfig.sh all
```
The generated files under `apple/Generated/` are intentionally ignored.
`VNIDROP_BUILD_TIME_UTC=YYYYMMDDHHMMSS` provides a deterministic clock for
tests; distribution builds normally use the current UTC time.
Before releasing: Before releasing:

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/../.." && pwd)"
resolver="$script_dir/resolve-version.sh"
output_dir="${VNIDROP_APPLE_XCCONFIG_DIR:-$repo_root/apple/Generated}"
mode="${1:-all}"
case "$mode" in
store|direct|all) ;;
*)
printf 'Usage: %s {store|direct|all}\n' "$0" >&2
exit 1
;;
esac
export VNIDROP_BUILD_TIME_UTC="${VNIDROP_BUILD_TIME_UTC:-$(date -u +%Y%m%d%H%M%S)}"
product_version="$("$resolver" product)"
mkdir -p "$output_dir"
write_config() {
local filename=$1
local build_field=$2
local destination="$output_dir/$filename"
local temporary
local build_number
build_number="$("$resolver" "$build_field")"
temporary="$(mktemp "$output_dir/.${filename}.XXXXXX")"
printf '%s\n' \
'// Generated by packaging/version/generate-apple-xcconfig.sh.' \
'// Regenerate this file instead of editing it.' \
'#include "../Signing.xcconfig"' \
'' \
"PRODUCT_VERSION = $product_version" \
"CURRENT_PROJECT_VERSION = $build_number" \
> "$temporary"
mv "$temporary" "$destination"
}
if [[ $mode == store || $mode == all ]]; then
write_config StoreVersion.xcconfig apple-store-build
fi
if [[ $mode == direct || $mode == all ]]; then
write_config DirectVersion.xcconfig apple-direct-build
fi

View File

@@ -1,6 +1,6 @@
[CmdletBinding()] [CmdletBinding()]
param( param(
[ValidateSet("Product", "Channel", "AndroidCode", "AppleBuild", "WindowsPackage", "Json", "Verify")] [ValidateSet("Product", "Channel", "AndroidCode", "AppleStoreBuild", "AppleDirectBuild", "WindowsPackage", "Json", "Verify")]
[string] $Field = "Verify", [string] $Field = "Verify",
[switch] $VerifyTag, [switch] $VerifyTag,
@@ -48,8 +48,31 @@ function Convert-CanonicalInteger {
$productVersion = Read-VersionProperty "PRODUCT_VERSION" $productVersion = Read-VersionProperty "PRODUCT_VERSION"
$releaseChannel = Read-VersionProperty "RELEASE_CHANNEL" $releaseChannel = Read-VersionProperty "RELEASE_CHANNEL"
$androidVersionCodeText = Read-VersionProperty "ANDROID_VERSION_CODE" $androidVersionCodeText = Read-VersionProperty "ANDROID_VERSION_CODE"
$appleBuildNumber = Read-VersionProperty "APPLE_BUILD_NUMBER"
$windowsVersionEpochText = Read-VersionProperty "WINDOWS_VERSION_EPOCH" $windowsVersionEpochText = Read-VersionProperty "WINDOWS_VERSION_EPOCH"
$buildTimeUtc = $env:VNIDROP_BUILD_TIME_UTC
if ([string]::IsNullOrWhiteSpace($buildTimeUtc)) {
$buildTimeUtc = [DateTime]::UtcNow.ToString(
"yyyyMMddHHmmss",
[Globalization.CultureInfo]::InvariantCulture
)
}
if ($buildTimeUtc -notmatch "^[0-9]{14}$") {
throw "VNIDROP_BUILD_TIME_UTC must use YYYYMMDDHHMMSS"
}
$parsedBuildTime = [DateTime]::MinValue
if (-not [DateTime]::TryParseExact(
$buildTimeUtc,
"yyyyMMddHHmmss",
[Globalization.CultureInfo]::InvariantCulture,
[Globalization.DateTimeStyles]::AssumeUniversal -bor [Globalization.DateTimeStyles]::AdjustToUniversal,
[ref] $parsedBuildTime
)) {
throw "VNIDROP_BUILD_TIME_UTC is not a valid UTC timestamp"
}
$appleBuildNumber = $parsedBuildTime.ToString(
"yyyyMMdd.HHmm.ss",
[Globalization.CultureInfo]::InvariantCulture
)
if ($productVersion -notmatch "^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$") { if ($productVersion -notmatch "^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$") {
throw "PRODUCT_VERSION must use canonical MAJOR.MINOR.PATCH integers" throw "PRODUCT_VERSION must use canonical MAJOR.MINOR.PATCH integers"
@@ -62,9 +85,6 @@ if ($releaseChannel -notmatch "^[a-z][a-z0-9-]*$") {
throw "RELEASE_CHANNEL contains unsupported characters" throw "RELEASE_CHANNEL contains unsupported characters"
} }
$androidVersionCode = Convert-CanonicalInteger "ANDROID_VERSION_CODE" $androidVersionCodeText 1 2100000000 $androidVersionCode = Convert-CanonicalInteger "ANDROID_VERSION_CODE" $androidVersionCodeText 1 2100000000
if ($appleBuildNumber -notmatch "^[1-9][0-9]*(\.[0-9]+){0,2}$") {
throw "APPLE_BUILD_NUMBER must contain one to three period-separated non-negative integers and start above zero"
}
$windowsVersionEpoch = Convert-CanonicalInteger "WINDOWS_VERSION_EPOCH" $windowsVersionEpochText 1 65535 $windowsVersionEpoch = Convert-CanonicalInteger "WINDOWS_VERSION_EPOCH" $windowsVersionEpochText 1 65535
$windowsMajor = $productMajor + $windowsVersionEpoch $windowsMajor = $productMajor + $windowsVersionEpoch
if ($windowsMajor -gt 65535) { if ($windowsMajor -gt 65535) {
@@ -80,7 +100,8 @@ $versionInfo = [ordered] @{
productVersion = $productVersion productVersion = $productVersion
releaseChannel = $releaseChannel releaseChannel = $releaseChannel
androidVersionCode = $androidVersionCode androidVersionCode = $androidVersionCode
appleBuildNumber = $appleBuildNumber appleStoreBuildNumber = $appleBuildNumber
appleDirectBuildNumber = $appleBuildNumber
windowsPackageVersion = $windowsPackageVersion windowsPackageVersion = $windowsPackageVersion
} }
@@ -88,10 +109,11 @@ switch ($Field) {
"Product" { $productVersion } "Product" { $productVersion }
"Channel" { $releaseChannel } "Channel" { $releaseChannel }
"AndroidCode" { $androidVersionCode } "AndroidCode" { $androidVersionCode }
"AppleBuild" { $appleBuildNumber } "AppleStoreBuild" { $appleBuildNumber }
"AppleDirectBuild" { $appleBuildNumber }
"WindowsPackage" { $windowsPackageVersion } "WindowsPackage" { $windowsPackageVersion }
"Json" { $versionInfo | ConvertTo-Json -Compress } "Json" { $versionInfo | ConvertTo-Json -Compress }
"Verify" { "Verify" {
"VniDrop $productVersion ($releaseChannel), Android $androidVersionCode, Apple $appleBuildNumber, MSIX $windowsPackageVersion" "VniDrop $productVersion ($releaseChannel), Android $androidVersionCode, Apple Store $appleBuildNumber, Apple Direct $appleBuildNumber, MSIX $windowsPackageVersion"
} }
} }

View File

@@ -35,8 +35,8 @@ validate_canonical_integer() {
product_version="$(read_property PRODUCT_VERSION)" product_version="$(read_property PRODUCT_VERSION)"
release_channel="$(read_property RELEASE_CHANNEL)" release_channel="$(read_property RELEASE_CHANNEL)"
android_version_code="$(read_property ANDROID_VERSION_CODE)" android_version_code="$(read_property ANDROID_VERSION_CODE)"
apple_build_number="$(read_property APPLE_BUILD_NUMBER)"
windows_version_epoch="$(read_property WINDOWS_VERSION_EPOCH)" windows_version_epoch="$(read_property WINDOWS_VERSION_EPOCH)"
build_time_utc="${VNIDROP_BUILD_TIME_UTC:-$(date -u +%Y%m%d%H%M%S)}"
[[ $product_version =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] || [[ $product_version =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]] ||
fail "PRODUCT_VERSION must use canonical MAJOR.MINOR.PATCH integers" fail "PRODUCT_VERSION must use canonical MAJOR.MINOR.PATCH integers"
@@ -47,9 +47,41 @@ validate_canonical_integer "PRODUCT_VERSION patch" "$product_patch" 0 65535
[[ $release_channel =~ ^[a-z][a-z0-9-]*$ ]] || [[ $release_channel =~ ^[a-z][a-z0-9-]*$ ]] ||
fail "RELEASE_CHANNEL must start with a lowercase letter and contain only lowercase letters, digits, and hyphens" fail "RELEASE_CHANNEL must start with a lowercase letter and contain only lowercase letters, digits, and hyphens"
validate_canonical_integer "ANDROID_VERSION_CODE" "$android_version_code" 1 2100000000 validate_canonical_integer "ANDROID_VERSION_CODE" "$android_version_code" 1 2100000000
[[ $apple_build_number =~ ^[1-9][0-9]*(\.[0-9]+){0,2}$ ]] ||
fail "APPLE_BUILD_NUMBER must contain one to three period-separated non-negative integers and start above zero"
validate_canonical_integer "WINDOWS_VERSION_EPOCH" "$windows_version_epoch" 1 65535 validate_canonical_integer "WINDOWS_VERSION_EPOCH" "$windows_version_epoch" 1 65535
[[ $build_time_utc =~ ^[0-9]{14}$ ]] ||
fail "VNIDROP_BUILD_TIME_UTC must use YYYYMMDDHHMMSS"
build_month="${build_time_utc:4:2}"
build_day="${build_time_utc:6:2}"
build_hour="${build_time_utc:8:2}"
build_minute="${build_time_utc:10:2}"
build_second="${build_time_utc:12:2}"
build_year="${build_time_utc:0:4}"
(( 10#$build_year >= 1 )) ||
fail "VNIDROP_BUILD_TIME_UTC contains an invalid year"
(( 10#$build_month >= 1 && 10#$build_month <= 12 )) ||
fail "VNIDROP_BUILD_TIME_UTC contains an invalid month"
case $((10#$build_month)) in
2)
max_build_day=28
if (( 10#$build_year % 400 == 0 ||
(10#$build_year % 4 == 0 && 10#$build_year % 100 != 0) )); then
max_build_day=29
fi
;;
4|6|9|11)
max_build_day=30
;;
*)
max_build_day=31
;;
esac
(( 10#$build_day >= 1 && 10#$build_day <= max_build_day )) ||
fail "VNIDROP_BUILD_TIME_UTC contains an invalid day"
(( 10#$build_hour <= 23 && 10#$build_minute <= 59 && 10#$build_second <= 59 )) ||
fail "VNIDROP_BUILD_TIME_UTC contains an invalid time"
apple_build_number="${build_time_utc:0:8}.${build_time_utc:8:4}.${build_time_utc:12:2}"
windows_major=$((10#$product_major + 10#$windows_version_epoch)) windows_major=$((10#$product_major + 10#$windows_version_epoch))
(( windows_major <= 65535 )) || (( windows_major <= 65535 )) ||
@@ -74,7 +106,10 @@ case "${1:-verify}" in
android-code) android-code)
printf '%s\n' "$android_version_code" printf '%s\n' "$android_version_code"
;; ;;
apple-build) apple-store-build)
printf '%s\n' "$apple_build_number"
;;
apple-direct-build)
printf '%s\n' "$apple_build_number" printf '%s\n' "$apple_build_number"
;; ;;
windows-package) windows-package)
@@ -82,14 +117,14 @@ case "${1:-verify}" in
;; ;;
verify) verify)
verify_tag verify_tag
printf 'VniDrop %s (%s), Android %s, Apple %s, MSIX %s\n' \ printf 'VniDrop %s (%s), Android %s, Apple Store %s, Apple Direct %s, MSIX %s\n' \
"$product_version" "$release_channel" "$android_version_code" \ "$product_version" "$release_channel" "$android_version_code" \
"$apple_build_number" "$windows_package_version" "$apple_build_number" "$apple_build_number" "$windows_package_version"
;; ;;
verify-tag) verify-tag)
verify_tag "${2:-}" verify_tag "${2:-}"
;; ;;
*) *)
fail "Usage: $0 {product|channel|android-code|apple-build|windows-package|verify|verify-tag [tag]}" fail "Usage: $0 {product|channel|android-code|apple-store-build|apple-direct-build|windows-package|verify|verify-tag [tag]}"
;; ;;
esac esac

View File

@@ -12,8 +12,7 @@ write_version() {
"PRODUCT_VERSION=$1" \ "PRODUCT_VERSION=$1" \
"RELEASE_CHANNEL=$2" \ "RELEASE_CHANNEL=$2" \
"ANDROID_VERSION_CODE=$3" \ "ANDROID_VERSION_CODE=$3" \
"APPLE_BUILD_NUMBER=$4" \ "WINDOWS_VERSION_EPOCH=$4" \
"WINDOWS_VERSION_EPOCH=$5" \
> "$scratch/version.properties" > "$scratch/version.properties"
} }
@@ -28,24 +27,41 @@ expect_failure() {
fi fi
} }
write_version 0.2.0 beta 2 2 1 export VNIDROP_BUILD_TIME_UTC=20260728143217
write_version 0.2.0 beta 2 1
[[ $(resolve product) == 0.2.0 ]] [[ $(resolve product) == 0.2.0 ]]
[[ $(resolve android-code) == 2 ]] [[ $(resolve android-code) == 2 ]]
[[ $(resolve apple-build) == 2 ]] [[ $(resolve apple-store-build) == 20260728.1432.17 ]]
[[ $(resolve apple-direct-build) == 20260728.1432.17 ]]
[[ $(resolve windows-package) == 1.2.0.0 ]] [[ $(resolve windows-package) == 1.2.0.0 ]]
resolve verify-tag v0.2.0 resolve verify-tag v0.2.0
expect_failure resolve verify-tag v1.0.0 expect_failure resolve verify-tag v1.0.0
write_version 1.0.0 stable 42 42 1 write_version 1.0.0 stable 42 1
[[ $(resolve windows-package) == 2.0.0.0 ]] [[ $(resolve windows-package) == 2.0.0.0 ]]
write_version 01.0.0 beta 2 2 1 write_version 01.0.0 beta 2 1
expect_failure resolve verify expect_failure resolve verify
write_version 0.2.0 beta 0 2 1 write_version 0.2.0 beta 0 1
expect_failure resolve verify expect_failure resolve verify
write_version 65535.0.0 stable 2 2 1 write_version 65535.0.0 stable 2 1
expect_failure resolve verify expect_failure resolve verify
VNIDROP_BUILD_TIME_UTC=20260728146000 expect_failure resolve verify
VNIDROP_BUILD_TIME_UTC=2026-07-28 expect_failure resolve verify
VNIDROP_BUILD_TIME_UTC=20260229080000 expect_failure resolve verify
write_version 0.2.0 beta 2 1
config_dir="$scratch/xcconfig"
VNIDROP_APPLE_XCCONFIG_DIR="$config_dir" \
"$script_dir/generate-apple-xcconfig.sh" all
grep -Fx "PRODUCT_VERSION = 0.2.0" "$config_dir/StoreVersion.xcconfig" >/dev/null
grep -Fx "CURRENT_PROJECT_VERSION = 20260728.1432.17" \
"$config_dir/StoreVersion.xcconfig" >/dev/null
grep -Fx "CURRENT_PROJECT_VERSION = 20260728.1432.17" \
"$config_dir/DirectVersion.xcconfig" >/dev/null
printf 'Version resolver tests passed.\n' printf 'Version resolver tests passed.\n'

View File

@@ -1,5 +1,4 @@
PRODUCT_VERSION=0.2.0 PRODUCT_VERSION=0.2.0
RELEASE_CHANNEL=beta RELEASE_CHANNEL=beta
ANDROID_VERSION_CODE=2 ANDROID_VERSION_CODE=2
APPLE_BUILD_NUMBER=2
WINDOWS_VERSION_EPOCH=1 WINDOWS_VERSION_EPOCH=1