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
path: |
apple/dist/VniDrop-*.dmg
apple/dist/VniDrop-*.build-info.json
apple/dist/appcast.xml
if-no-files-found: error
retention-days: 14

1
.gitignore vendored
View File

@@ -25,3 +25,4 @@ config.override.mk
output/
.screenshots
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: 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: 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-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; }
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
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 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
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.
CODE_SIGNING_ALLOWED = NO
CODE_SIGNING_REQUIRED = NO

View File

@@ -1,9 +1,11 @@
# XcodeGen spec for the native SwiftUI VniDrop app (iOS/iPadOS/macOS).
# 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
# - Localization: (cd localization && bun run src/cli.ts generate)
# -> VniDrop/Resources/Localizable.xcstrings, VniDrop/Generated/L10n.swift
# - Versions: packaging/version/generate-apple-xcconfig.sh all
# -> Generated/StoreVersion.xcconfig, Generated/DirectVersion.xcconfig
name: VniDrop
options:
bundleIdPrefix: com.vnidrop
@@ -50,10 +52,6 @@ packages:
targetTemplates:
AppBase:
type: application
configFiles:
Debug: Signing.xcconfig
Release: Signing.xcconfig
Release-Direct: Signing.xcconfig
sources:
- path: VniDrop
excludes:
@@ -66,7 +64,6 @@ targetTemplates:
PRODUCT_NAME: VniDrop
PRODUCT_BUNDLE_IDENTIFIER: com.vnidrop.app
MARKETING_VERSION: "$(PRODUCT_VERSION)"
CURRENT_PROJECT_VERSION: "$(APPLE_BUILD_NUMBER)"
GENERATE_INFOPLIST_FILE: NO
INFOPLIST_FILE: VniDrop/Resources/Info.plist
CODE_SIGN_ENTITLEMENTS: VniDrop/Resources/VniDrop.entitlements
@@ -114,6 +111,10 @@ targets:
VniDrop:
templates: [AppBase]
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
# and self-updates via Sparkle. DIRECT_DISTRIBUTION gates all Sparkle code so the
@@ -121,6 +122,10 @@ targets:
VniDropDirect:
templates: [AppBase]
supportedDestinations: [macOS]
configFiles:
Debug: Generated/DirectVersion.xcconfig
Release: Generated/DirectVersion.xcconfig
Release-Direct: Generated/DirectVersion.xcconfig
settings:
base:
SWIFT_ACTIVE_COMPILATION_CONDITIONS: "$(inherited) DIRECT_DISTRIBUTION"

View File

@@ -29,10 +29,13 @@ SCHEME="VniDropDirect"
CONFIG="Release-Direct"
APP_NAME="VniDrop"
VERSION_RESOLVER="$REPO_ROOT/packaging/version/resolve-version.sh"
VERSION_CONFIG_GENERATOR="$REPO_ROOT/packaging/version/generate-apple-xcconfig.sh"
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
BUILD_METADATA="$DIST_DIR/$APP_NAME-$VERSION.build-info.json"
# --- Resolve signing identity ------------------------------------------------
if [ -z "${DEVELOPER_ID_APP:-}" ]; then
@@ -57,6 +60,7 @@ echo " team: ${DEVELOPMENT_TEAM:-<unknown>}"
echo "==> Building Rust core (release)"
CARGO_PROFILE_RELEASE_LTO=false "$SCRIPT_DIR/build-core.sh" release
echo "==> Regenerating Xcode project"
"$VERSION_CONFIG_GENERATOR" all
( cd "$APPLE_DIR" && xcodegen generate >/dev/null )
rm -rf "$BUILD_DIR" && mkdir -p "$BUILD_DIR" "$DIST_DIR"
@@ -88,6 +92,18 @@ xcodebuild -exportArchive \
-exportOptionsPlist "$EXPORT_OPTS"
APP="$EXPORT_DIR/$APP_NAME.app"
[ -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 -----------------------------------------------------------
DMG="$DIST_DIR/$APP_NAME-$VERSION.dmg"
@@ -133,7 +149,18 @@ else
fi
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 " dmg: $DMG"
echo " version: $VERSION"
echo " build: $BUILD_NUMBER"
echo " size: $SIZE bytes"

View File

@@ -54,10 +54,6 @@ val androidVersionCode = canonicalInteger(
requiredVersionProperty("ANDROID_VERSION_CODE"),
1L..2_100_000_000L,
).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(
"WINDOWS_VERSION_EPOCH",
requiredVersionProperty("WINDOWS_VERSION_EPOCH"),
@@ -73,7 +69,6 @@ val windowsPackageVersion =
extra["vnidrop.productVersion"] = productVersion
extra["vnidrop.releaseChannel"] = releaseChannel
extra["vnidrop.androidVersionCode"] = androidVersionCode
extra["vnidrop.appleBuildNumber"] = appleBuildNumber
extra["vnidrop.windowsPackageVersion"] = windowsPackageVersion
tasks.register("verifyVersion") {
@@ -83,6 +78,5 @@ tasks.register("verifyVersion") {
inputs.property("productVersion", productVersion)
inputs.property("releaseChannel", releaseChannel)
inputs.property("androidVersionCode", androidVersionCode)
inputs.property("appleBuildNumber", appleBuildNumber)
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
on a closed-testing track; this pipeline cannot publish to production.
To release, first update and merge `version.properties`, including monotonic
Android and Apple build numbers. Then create and push the matching tag:
To release, first update and merge `version.properties`, including a monotonic
Android version code. Apple Store and Direct build numbers are derived
independently at build time. Then create and push the matching tag:
```bash
git tag -s v0.2.0 -m "VniDrop 0.2.0"

View File

@@ -45,7 +45,6 @@ verify_checksum_file() {
version="$("$resolver" product)"
android_code="$("$resolver" android-code)"
apple_build="$("$resolver" apple-build)"
windows_package="$("$resolver" windows-package)"
"$resolver" verify >/dev/null
[[ $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')"
dmg="$(find_single "$input_dir/macos" '*.dmg' 'macOS DMG')"
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_metadata="$(find_single "$input_dir/play" 'play-release.json' 'Play release metadata')"
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 "$msix") == "VniDrop_${version}_x64.msix" ]]
[[ $(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')"
rpm_checksum="$(find_single "$input_dir/rpm" '*.sha256' 'RPM checksum')"
@@ -120,7 +128,7 @@ jq -n \
--arg tag "$source_tag" \
--arg commit "$source_commit" \
--arg androidVersionCode "$android_code" \
--arg appleBuildNumber "$apple_build" \
--arg appleDirectBuildNumber "$apple_direct_build" \
--arg windowsPackageVersion "$windows_package" \
--arg windowsMsixUpload "$(basename "$msixupload")" \
--arg windowsMsixUploadSha256 "$(sha256sum "$msixupload" | awk '{print $1}')" \
@@ -135,7 +143,7 @@ jq -n \
sourceCommit: $commit,
platformVersions: {
androidVersionCode: ($androidVersionCode | tonumber),
appleBuildNumber: $appleBuildNumber,
appleDirectBuildNumber: $appleDirectBuildNumber,
windowsPackageVersion: $windowsPackageVersion
},
play: {

View File

@@ -19,6 +19,7 @@ mkdir -p \
version="$("$repo_root/packaging/version/resolve-version.sh" product)"
android_code="$("$repo_root/packaging/version/resolve-version.sh" android-code)"
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 '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 '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 \
--arg releaseName "$version" \
--argjson versionCode "$android_code" \
@@ -94,6 +106,8 @@ done < <(find "$output_dir" -maxdepth 1 -type f -print | sort)
[[ ${actual_public_files[*]} == "${expected_public_files[*]}" ]]
[[ $(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 '.windowsStore.publicReleaseAsset' "$output_dir/release-manifest.json") == false ]]
(

View File

@@ -1,11 +1,12 @@
# Application versioning
`version.properties` at the repository root is the single source of truth for
the VniDrop application version. Platform projects and release workflows read
that file rather than accepting independent version overrides.
the VniDrop product version and persistent store counters. Platform projects
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,
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
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 |
| --- | --- | --- |
| Android | `PRODUCT_VERSION` | `ANDROID_VERSION_CODE` |
| Apple | `PRODUCT_VERSION` | `APPLE_BUILD_NUMBER` |
| Linux and direct macOS | `PRODUCT_VERSION` | Native package revision |
| Apple Store | `PRODUCT_VERSION` | Derived UTC `YYYYMMDD.HHMM.SS` |
| 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 |
| 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`
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
changed Windows Store package must increment the product version because the
Store-reserved fourth component cannot carry a rebuild number.
Every Android upload must increment `ANDROID_VERSION_CODE`. Apple build numbers
are derived at build time by `apple-store-build` and `apple-direct-build`; they
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:

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()]
param(
[ValidateSet("Product", "Channel", "AndroidCode", "AppleBuild", "WindowsPackage", "Json", "Verify")]
[ValidateSet("Product", "Channel", "AndroidCode", "AppleStoreBuild", "AppleDirectBuild", "WindowsPackage", "Json", "Verify")]
[string] $Field = "Verify",
[switch] $VerifyTag,
@@ -48,8 +48,31 @@ function Convert-CanonicalInteger {
$productVersion = Read-VersionProperty "PRODUCT_VERSION"
$releaseChannel = Read-VersionProperty "RELEASE_CHANNEL"
$androidVersionCodeText = Read-VersionProperty "ANDROID_VERSION_CODE"
$appleBuildNumber = Read-VersionProperty "APPLE_BUILD_NUMBER"
$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]*)$") {
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"
}
$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
$windowsMajor = $productMajor + $windowsVersionEpoch
if ($windowsMajor -gt 65535) {
@@ -80,7 +100,8 @@ $versionInfo = [ordered] @{
productVersion = $productVersion
releaseChannel = $releaseChannel
androidVersionCode = $androidVersionCode
appleBuildNumber = $appleBuildNumber
appleStoreBuildNumber = $appleBuildNumber
appleDirectBuildNumber = $appleBuildNumber
windowsPackageVersion = $windowsPackageVersion
}
@@ -88,10 +109,11 @@ switch ($Field) {
"Product" { $productVersion }
"Channel" { $releaseChannel }
"AndroidCode" { $androidVersionCode }
"AppleBuild" { $appleBuildNumber }
"AppleStoreBuild" { $appleBuildNumber }
"AppleDirectBuild" { $appleBuildNumber }
"WindowsPackage" { $windowsPackageVersion }
"Json" { $versionInfo | ConvertTo-Json -Compress }
"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)"
release_channel="$(read_property RELEASE_CHANNEL)"
android_version_code="$(read_property ANDROID_VERSION_CODE)"
apple_build_number="$(read_property APPLE_BUILD_NUMBER)"
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]*)$ ]] ||
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-]*$ ]] ||
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
[[ $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
[[ $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 <= 65535 )) ||
@@ -74,7 +106,10 @@ case "${1:-verify}" in
android-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"
;;
windows-package)
@@ -82,14 +117,14 @@ case "${1:-verify}" in
;;
verify)
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" \
"$apple_build_number" "$windows_package_version"
"$apple_build_number" "$apple_build_number" "$windows_package_version"
;;
verify-tag)
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

View File

@@ -12,8 +12,7 @@ write_version() {
"PRODUCT_VERSION=$1" \
"RELEASE_CHANNEL=$2" \
"ANDROID_VERSION_CODE=$3" \
"APPLE_BUILD_NUMBER=$4" \
"WINDOWS_VERSION_EPOCH=$5" \
"WINDOWS_VERSION_EPOCH=$4" \
> "$scratch/version.properties"
}
@@ -28,24 +27,41 @@ expect_failure() {
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 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 verify-tag v0.2.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 ]]
write_version 01.0.0 beta 2 2 1
write_version 01.0.0 beta 2 1
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
write_version 65535.0.0 stable 2 2 1
write_version 65535.0.0 stable 2 1
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'

View File

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