From c6655da7db297fb9994cc2ea59b3518982b08516 Mon Sep 17 00:00:00 2001 From: Hammed Abass Date: Tue, 28 Jul 2026 11:27:45 +0200 Subject: [PATCH] refactor(version): derive Apple build numbers --- .github/workflows/apple-release.yml | 1 + .gitignore | 1 + Makefile | 7 ++- apple/README.md | 6 +++ apple/Signing.xcconfig | 3 -- apple/project.yml | 17 ++++--- apple/scripts/build-dmg.sh | 29 +++++++++++- build.gradle.kts | 6 --- packaging/release/README.md | 5 +- packaging/release/assemble-release.sh | 14 ++++-- packaging/release/test-assemble-release.sh | 14 ++++++ packaging/version/README.md | 32 +++++++++---- packaging/version/generate-apple-xcconfig.sh | 48 +++++++++++++++++++ packaging/version/resolve-version.ps1 | 38 +++++++++++---- packaging/version/resolve-version.sh | 49 +++++++++++++++++--- packaging/version/test-version.sh | 32 +++++++++---- version.properties | 1 - 17 files changed, 248 insertions(+), 55 deletions(-) create mode 100755 packaging/version/generate-apple-xcconfig.sh diff --git a/.github/workflows/apple-release.yml b/.github/workflows/apple-release.yml index e4de19e..da32c60 100644 --- a/.github/workflows/apple-release.yml +++ b/.github/workflows/apple-release.yml @@ -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 diff --git a/.gitignore b/.gitignore index 5b43b44..048c9d4 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ config.override.mk output/ .screenshots apple/RELEASE-MACOS.md +apple/Generated/*.xcconfig diff --git a/Makefile b/Makefile index e33de0e..8928169 100644 --- a/Makefile +++ b/Makefile @@ -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. diff --git a/apple/README.md b/apple/README.md index d7b8b0c..008ece4 100644 --- a/apple/README.md +++ b/apple/README.md @@ -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: diff --git a/apple/Signing.xcconfig b/apple/Signing.xcconfig index 396ad11..da695ba 100644 --- a/apple/Signing.xcconfig +++ b/apple/Signing.xcconfig @@ -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 diff --git a/apple/project.yml b/apple/project.yml index cfa2d8c..e883004 100644 --- a/apple/project.yml +++ b/apple/project.yml @@ -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" diff --git a/apple/scripts/build-dmg.sh b/apple/scripts/build-dmg.sh index 8ee8af5..bb0f6a5 100755 --- a/apple/scripts/build-dmg.sh +++ b/apple/scripts/build-dmg.sh @@ -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:-}" 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" diff --git a/build.gradle.kts b/build.gradle.kts index db5d558..ca0887e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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) } diff --git a/packaging/release/README.md b/packaging/release/README.md index d86a4ae..a999e03 100644 --- a/packaging/release/README.md +++ b/packaging/release/README.md @@ -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" diff --git a/packaging/release/assemble-release.sh b/packaging/release/assemble-release.sh index dd282d4..398025e 100755 --- a/packaging/release/assemble-release.sh +++ b/packaging/release/assemble-release.sh @@ -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: { diff --git a/packaging/release/test-assemble-release.sh b/packaging/release/test-assemble-release.sh index b434297..578191a 100755 --- a/packaging/release/test-assemble-release.sh +++ b/packaging/release/test-assemble-release.sh @@ -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 ]] ( diff --git a/packaging/version/README.md b/packaging/version/README.md index 14af240..27ec40a 100644 --- a/packaging/version/README.md +++ b/packaging/version/README.md @@ -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: diff --git a/packaging/version/generate-apple-xcconfig.sh b/packaging/version/generate-apple-xcconfig.sh new file mode 100755 index 0000000..c1bd45c --- /dev/null +++ b/packaging/version/generate-apple-xcconfig.sh @@ -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 diff --git a/packaging/version/resolve-version.ps1 b/packaging/version/resolve-version.ps1 index 64be60c..02d4467 100644 --- a/packaging/version/resolve-version.ps1 +++ b/packaging/version/resolve-version.ps1 @@ -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" } } diff --git a/packaging/version/resolve-version.sh b/packaging/version/resolve-version.sh index 1d7b191..589381f 100755 --- a/packaging/version/resolve-version.sh +++ b/packaging/version/resolve-version.sh @@ -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 diff --git a/packaging/version/test-version.sh b/packaging/version/test-version.sh index 8eacbb3..e883473 100755 --- a/packaging/version/test-version.sh +++ b/packaging/version/test-version.sh @@ -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' diff --git a/version.properties b/version.properties index ee10ff4..57070b6 100644 --- a/version.properties +++ b/version.properties @@ -1,5 +1,4 @@ PRODUCT_VERSION=0.2.0 RELEASE_CHANNEL=beta ANDROID_VERSION_CODE=2 -APPLE_BUILD_NUMBER=2 WINDOWS_VERSION_EPOCH=1