Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Bug][Proposal]: New Termux app versionCode spec to solve app store issues #4012

Open
agnostic-apollo opened this issue Jun 11, 2024 · 10 comments

Comments

@agnostic-apollo
Copy link
Member

agnostic-apollo commented Jun 11, 2024

Android and Google PlayStore/F-Droid stores use the versionCode positive integer value of the currently installed app to decide whether the new APK to be installed or one hosted by the store is an updated version or not. If the versionCode of the new APK is higher, only then it is considered an update and allowed to be normally installed over the currently installed app. The versionName string value of apps is not used in this decision.

The max value for versionCode that Google PlayStore allows is 2 100 000 000 (2100000000), Android also internally stores it as an int and java INT_MAX value is only slightly higher 2 147 483 647.

Since termux-apps use semantic versioning 2.0 for versionName in the format x.y.x since 0.118.0 release, there needs to be some way for mapping the versionName to a versionCode while keeping it different for different install sources so that their app stores don't show updates of other sources. This is a problem for Google PlayStore because it does not check signatures of the currently installed app APK against the latest app APK hosted by the store that is to be updated, and it only compares the versionCode. So if the device has an APK installed from a different install source with a different signature, but its versionCode is lower than the latest APK hosted in the store, then PlayStore will attempt to download and install it and then fail with a Can't install Termux generic UI error, with the actual error Finsky Submitter: commit error message INSTALL_FAILED_UPDATE_INCOMPATIBLE: Existing package com.termux signatures do not match newer version; ignoring! logged to logcat that is returned by the Android package installer. User can manually disable auto update for the app from its app page options button, but any updates will still show in the PlayStore app updates list. Basically, to solve this, the versionCode of app APKs hosted by the "broken" stores must be lower than other install sources.

Note that F-Droid store does not show updates for apps in the app updates list if they are signed with a different signing key. The APK signing certificate SHA-256 digest is stored in the packages: <package_name>: versions: <version_id>: manifest: signer: sha256: 0 field of the index that F-Droid repository maintains for all hosted app APKs and it does not need to download the actual APK to find it. The signer field is stored in the manifest_signer_sha256 field of the Version table of the /data/data/org.fdroid.fdroid/databases/fdroid_db database that F-Droid app maintains. (1, 2) If the user manually tries to update an app from its app page, it is prevented with the The new version is signed with a different key to the old one. To install the new version, the old one must be uninstalled first. Please do this and try again. (Note that uninstalling will erase any internal data stored by the app) (1, 2) error, without actually trying to update the app and then failing.

An incremental versionCode cannot be used, as that would not allow patch releases. For example, 0.118.1 is released as 1000, then 0.119.0-beta.1 is released as 1001. If 0.118.1 F-Droid build fails or a bug is found after the release, then 0.118.2 patch version update may need to be released, but if its now released as 1002, it will be considered as higher version than 0.119.0-beta.1 (1001), which it won't be. So each major/minor/patch update must have sufficient reserved slots after their next respective increment. Additionally, there must be reserved slots for beta releases before a stable release as well.

The following versioning spec is proposed for mapping the versionName to a versionCode while also keeping a priority for different install sources. As the Google PlayStore allows max value 2 100 000 000 for a versionCode, but since the first and second section cannot go above 2 100, the max value is limited to 1 999 999 999 in the spec. Google PlayStore also suggests a similar versionCode spec.

noop  isp  major minor patch beta/stable
0     0    00    000   00    0
  • noop: 0-1 (currently not used)
  • release_variant: 1-9 (other values could be used for other stores like Samsung store)
    • 1 - PlayStore (lowest priority)
    • 5 - F-Droid or other sources like planned Termux site
    • 7 - GitHub and custom builds (highest priority)
  • major: 0-99
  • minor: 0-999
  • patch: 0-99
  • beta/stable: 0-9

The effectively allows 1000 minor releases for each of the 100 major releases. So a total of 100 x 1000 = 1,00,000 (1 hundred thousand) major/minor releases. If the 100 patch releases are also included, then total releases allowed would be 100 x 1000 x 100 = 10,000,000 (10 million). Even though the semantic versioning spec does not limit the size of major/minor/patch, the 1,00,000 releases should be sufficient for Termux's future. It will allow more than 5 releases to be made per day for the next 50 years (90000). Different install sources will also never conflict and will also have 1,00,000 releases each. The noop could possibly be used for targetSdkVersion variants or some other config, or could be used to expand versions themselves.

Following are examples of versionCode mapping from versionName with the noop and release_variant parts not set (= 0).

0 0 00 118 000 - 0.118.0
0 0 00 118 010 - 0.118.1
0 0 00 118 020 - 0.118.2
0 0 00 118 090 - 0.118.9

0 0 00 119 000 - 0.119.0-beta.1
0 0 00 119 001 - 0.119.0-beta.2
0 0 00 119 002 - 0.119.0

0 0 00 120 000 - 0.120.0-beta.1
0 0 00 120 008 - 0.120.0-beta.9
0 0 00 120 009 - 0.120.0

0 0 00 120 010 - 0.120.1

0 0 00 120 990 - 0.120.99

0 0 00 999 000 - 0.999.0
0 0 00 999 990 - 0.999.99

0 0 01 000 000 - 1.0.0

0 0 99 000 000 - 99.0.0

Following includes the release_variant parts for different versions.

0 1 00 108 010 - 0.108.1       - PlayStore
0 1 00 119 002 - 0.119.0       - PlayStore

0 5 00 118 010 - 0.118.1 -       F-Droid
0 5 00 119 000 - 0.119.0-beta.1 - F-Droid
0 5 00 119 001 - 0.119.0-beta.2 - F-Droid
0 5 00 119 002 - 0.119.0       - F-Droid

0 7 00 118 010 - 0.118.1 -       GitHub
0 7 00 119 000 - 0.119.0-beta.1 - GitHub
0 7 00 119 001 - 0.119.0-beta.2 - GitHub
0 7 00 119 002 - 0.119.0       - GitHub

At build time, the value release_variant x 1 00 000 000 can be added to the versionCode.

  • PlayStore: 1 00 000 000
  • F-Droid: 5 00 000 000
  • GitHub: 7 00 000 000

The release_variant can be set by exporting the $TERMUX_APP_BUILD__VERSION_CODE_RELEASE_VARIANT environment variable at build time, with the default value 7. For F-Droid, a pull request will need to be sent to export the value in the prebuild step of the com.termux package metadata file.

prebuild:
        - export TERMUX_APP_BUILD__VERSION_CODE_RELEASE_VARIANT=5

However, since F-Droid cannot dynamically detect generated versionCode, the versionCode would need to be hardcoded in app/build.gradle build, like 0 5 00 118 010 (versionCode 500118010) for next versionName 0.118.1. It would be much simpler to just use priority 5 for both GitHub and F-Droid, and possibly other non-broken sources/stores, like Termux site. This would also not require sending a pull request and no special logic will be need to be added to handle F-Droid requirement by first subtracting 5 00 000 000 from versionCode, and then adding $TERMUX_APP_BUILD__VERSION_CODE_RELEASE_VARIANT.

Related #4000

@agnostic-apollo
Copy link
Member Author

agnostic-apollo commented Jun 11, 2024

@Grimler91 @sylirre @fornwall Let me know what you think.

@immanuelfodor

This comment was marked as off-topic.

@sylirre
Copy link
Member

sylirre commented Jun 12, 2024

Looks good and it would be enough to have just two version priorities (PS and non-PS releases, non-PS preferred).

@agnostic-apollo
Copy link
Member Author

agnostic-apollo commented Jun 12, 2024

Yeah, true, less work and divergence too. If we ever need a priority above default 5 in future, we can use it then. Hopefully math checks out.

@agnostic-apollo
Copy link
Member Author

A similar proposal was made to semver at semver/semver#516 without the noop and install_source_priority fields of course, but it wouldn't be humanly readable as hex based and will require running code/maths by the dev to generate it on each update. However, it is reversible, unlike mine, its T field allows generation of version name back from the version code. In mine, we won't be able to find out whether it is a stable or beta release as that info is not kept. Will have to look into possible version ranges for it while having priority and keeping it below 2100000000.

@Grimler91
Copy link
Member

Neat scheme, looks to me like it would work. Being able to tell if a version is beta or not from versioncode is nice but I guess not necessary

@fornwall
Copy link
Member

Looks like it could work!

As a version scheme like this is not something to rush into (given the mess of making any changes to it later on), it might be good to give it some time&thought and room for feedback.

So perhaps a 0.118.1 build can be released ASAP/today (with #4005 merged) with 1000 as a special one-time version code, to fix the pressing need of people being prompted to update away from F-Droid to Google Play? And then a more proper/systematic version code can be introduced in the next release.

@agnostic-apollo
Copy link
Member Author

agnostic-apollo commented Jun 13, 2024

The android-app-semver is a restricted semantic versioning 2.0 compliant spec for Android app versionName and versionCode. Unlike the semver 2.0 spec format major.minor.patch(-prerelease)(+buildmetadata), it does not support (+buildmetadata) and only allows limited types and revisions for (-prerelease) in the format major.minor.patch(-(alpha|beta|rc).<revision>). It is based on semver/semver#516 and it stores a versionName in a 30-bit integer versionCode, with noop and release_variant fields added.

The full 32-bits of an integer cannot be used as the max value for versionCode that Google PlayStore allows is 2100000000 (31-bits). As the spec requires the possibility for setting all the bits to 1, the actual max possible usable value is 1073741823 30-bits instead. Furthermore, as per spec details below and its ranges, the 63.511.31 (1073741808) version is the max possible version and since it is a stable release, its revision (last 4 bits) must be 0 and no additional bits can be set in those. Due to this, all the versions till 1073741823 cannot be used and the actual limit is 1073741808, i.e 15 versions less (1073741823 - 1073741808 = 15).

1111101001010110111010100000000 (2100000000)
 111111111111111111111111111111 (1073741823)
 111111111111111111111111110000 (1073741808)

The versionCode format is:

30 29    26          20                11        6   4       0
 ├─┼─┴─┴─┼─┴─┴─┴─┴─┴─┼─┴─┴─┴─┴─┴─┴─┴─┴─┼─┴─┴─┴─┴─┼─┴─┼─┴─┴─┴─┤
 │N|  RV |   major   │       minor     │  patch  │RT │  rev  │
 └─┴─────┴───────────┴─────────────────┴─────────┴───┴───────┘

 1  111    111111        111111111       11111    11   1111

The fields are:

  • noop: 1-bit - 0-1 (currently not used)
  • release_variant: 3-bit - 0-7
  • major: 6-bit - 0-63
  • minor: 9-bit - 0-511
  • patch: 5-bit - 0-31
  • release_type: 2-bit - 0-3
    • 0 - alpha
    • 1 - beta
    • 2 - rc
    • 3 - stable (revision MUST be 0)
  • revision: 4-bit - 1-15

The regex ^([0-9]|[1-5][0-9]|6[0-3])\.([0-9]|[1-9][0-9]|[1-4][0-9][0-9]|50[0-9]|51[0-1])\.([0-9]|[1-2][0-9]|3[0-1])(-(alpha|beta|rc)\.([1-9]|1[0-5]))?$ can be used to match a valid versionName for the spec and capture its parts as per following:

  • ^ - start anchor.
  • ([0-9]|[1-5][0-9]|6[0-3]) - major 0-63 (capturing group 1).
  • \. - version dot . separator.
  • ([0-9]|[1-9][0-9]|[1-4][0-9][0-9]|50[0-9]|51[0-1]) - minor 0-511 (capturing group 2).
  • \. - version dot . separator.
  • ([0-9]|[1-2][0-9]|3[0-1]) - patch 0-31 (capturing group 3).
  • (-(alpha|beta|rc)\.([0-9]|1[0-5]))? - optional pre-release (capturing group 4).
    • - - pre-release start.
    • (alpha|beta|rc) - release_type_name (capturing group 5).
    • \. - identifier dot . separator.
    • ([1-9]|1[0-5]) - revision 1-15 (capturing group 6).
  • $ - end anchor.

The spec effectively allows 511 minor releases for each of the 63 major releases. So a total of 63 x 511 = 32,193 (32 thousand) major/minor releases. If the 31 patch releases are also included, then total releases allowed would be 63 x 511 x 31 = 997,983 (~1 million). Even though the semantic versioning spec does not limit the size of major/minor/patch, the 32,193 releases should be sufficient for an app's future. It will allow 1.8 releases per day for the next 50 years (1.8×30×12×50 = 32400). Different install sources will also never conflict and will also have 32,193 releases each. The noop could possibly be used for targetSdkVersion variants or some other config, or could be used to expand versions themselves. The release_variant field can also be used in pairs like 0-1, 2-3, 4-5, 6-7 to effectively double/quadruple the possible releases per variant. If the normal version codes run out, the remaining 2100000000 − 1073741823 = 1,026,258,177 (1 billion) version codes can still be used with a different spec, but hopefully, I would be long dead before even the normal limit crosses.

Apps can use the release_variant as an install source priority, like:

  • 0 - PlayStore.
  • 4 - F-Droid, GitHub releases/actions and other planned sources like Termux site. (default)

The only other issue with the android-app-semver spec is that it does not preserve (+buildmetadata), nor would it be technically possible to store it in the 30-bit integer, and it were, then it affect the version precedence, which normally buildmetadata does not in semver.

Termux app GitHub action builds use the +commit_hash as buildmetadata to distinguish internal build from GitHub release builds and to know which commit was the build generated from, this strategy is used by other apps too. For Termux, GitHub action builds could use a higher install source priority with the release_variant field, like 6 than the default 4, but it still wouldn't tell which commit the build was generated from, and so the version name would need to be relied upon for that anyways. Instead the versionCode (along with versionName) could be bumped as a pre-release when significant changes have been made after a stable release.


Following are examples of versionCode mapping from versionName with the noop field not set (= 0).

version_name:           0.118.0, version_code:  268677168 (0 100 000000 001110110 00000 11 0000), release_variant: 4
version_name:           0.118.1, version_code:  268677232 (0 100 000000 001110110 00001 11 0000), release_variant: 4
version_name:           0.118.2, version_code:  268677296 (0 100 000000 001110110 00010 11 0000), release_variant: 4
version_name:          0.118.31, version_code:  268679152 (0 100 000000 001110110 11111 11 0000), release_variant: 4

version_name:    0.119.0-beta.1, version_code:  268679185 (0 100 000000 001110111 00000 01 0001), release_variant: 4
version_name:    0.119.0-beta.2, version_code:  268679186 (0 100 000000 001110111 00000 01 0010), release_variant: 4
version_name:           0.119.0, version_code:  268679216 (0 100 000000 001110111 00000 11 0000), release_variant: 4

version_name:   0.120.0-alpha.1, version_code:  268681217 (0 100 000000 001111000 00000 00 0001), release_variant: 4
version_name:   0.120.0-alpha.2, version_code:  268681218 (0 100 000000 001111000 00000 00 0010), release_variant: 4
version_name:    0.120.0-beta.1, version_code:  268681233 (0 100 000000 001111000 00000 01 0001), release_variant: 4
version_name:   0.120.0-beta.15, version_code:  268681247 (0 100 000000 001111000 00000 01 1111), release_variant: 4
version_name:      0.120.0-rc.1, version_code:  268681249 (0 100 000000 001111000 00000 10 0001), release_variant: 4
version_name:           0.120.0, version_code:  268681264 (0 100 000000 001111000 00000 11 0000), release_variant: 4

version_name:           0.120.1, version_code:  268681328 (0 100 000000 001111000 00001 11 0000), release_variant: 4

version_name:          0.120.31, version_code:  268683248 (0 100 000000 001111000 11111 11 0000), release_variant: 4

version_name:           0.511.0, version_code:  269482032 (0 100 000000 111111111 00000 11 0000), release_variant: 4
version_name:          0.511.31, version_code:  269484016 (0 100 000000 111111111 11111 11 0000), release_variant: 4

version_name:             1.0.0, version_code:  269484080 (0 100 000001 000000000 00000 11 0000), release_variant: 4

version_name:            63.0.0, version_code:  334495792 (0 100 111111 000000000 00000 11 0000), release_variant: 4

version_name:         63.511.31, version_code:  335544304 (0 100 111111 111111111 11111 11 0000), release_variant: 4

version_name:   63.511.31-rc.15, version_code: 1073741807 (1 111 111111 111111111 11111 10 1111), release_variant: 7, noop: 1
version_name:         63.511.31, version_code: 1073741808 (1 111 111111 111111111 11111 11 0000), release_variant: 7, noop: 1

Following are examples of versions with different release_variant.

version_name:           0.108.1, version_code:   67330160 (0 001 000000 001101100 00001 11 0000), release_variant: 1
version_name:           0.119.0, version_code:   67352624 (0 001 000000 001110111 00000 11 0000), release_variant: 1

version_name:           0.118.1, version_code:  268677232 (0 100 000000 001110110 00001 11 0000), release_variant: 4
version_name:    0.119.0-beta.1, version_code:  268679185 (0 100 000000 001110111 00000 01 0001), release_variant: 4
version_name:    0.119.0-beta.2, version_code:  268679186 (0 100 000000 001110111 00000 01 0010), release_variant: 4
version_name:           0.119.0, version_code:  268679216 (0 100 000000 001110111 00000 11 0000), release_variant: 4

Following is the android-app-semver-spec-utils.sh script which provides utils to convert versions that are compliant with the android-app-semver spec.

#!/usr/bin/bash
# shellcheck shell=bash

AASSU__LOG_LEVEL=1
AASSU__DEFAULT_NOOP=0
AASSU__DEFAULT_RELEASE_VARIANT=4
AASSU__MAX_VERSION_CODE=1073741808

show_help() {

    cat <<'HELP_EOF'
android-app-semver-spec-utils.sh provides utils to convert versions
that are compliant with the `android-app-semver` spec.


Available commands:
    name-to-code     Convert a version name to a version code.
    code-to-name     Convert a version code to a version name.


Usage:
  android-app-semver-spec-utils.sh [command_options] name-to-code \
    <version_name> [[release_variant] [noop]]
  android-app-semver-spec-utils.sh [command_options] code-to-name \
    <version_code>


Available command_options:
  [ -h | --help ]    Display this help screen.
  [ -v ]             Set log level to `DEBUG`.



For the `name-to-code` command:
- The `release_variant` and `noop` arguments are optional. Either
  both should be passed, or none, or only `release_variant`.
  The default value is `4` for `release_variant` and `0` for `noop`.
- The `version_name` must be in the format
  `major.minor.patch(-(alpha|beta|rc).<revision>)`.
  - The `major` value must be between `0-63` (inclusive).
  - The `minor` value must be between `0-511` (inclusive).
  - The `patch` value must be between `0-31` (inclusive).
  - The `(-(alpha|beta|rc).<revision>)` part is optional and if not
    passed, it is conidered a `stable` `release_type`. If passed, then
    `revision` must be between `1-15` (inclusive), like `-beta.1`.
- The `release_variant` value must be between `0-7` (inclusive).
- The `noop` value must be between `0-1` (inclusive).

Examples:
android-app-semver-spec-utils.sh name-to-code 0.1.0
android-app-semver-spec-utils.sh name-to-code 0.1.0-beta.1 0
android-app-semver-spec-utils.sh name-to-code 63.511.31 7 1



For the `code-to-name` command:
- The `version_code` value must be between `0-1073741808` (inclusive)
  and must be a valid value as per spec.

Examples:
android-app-semver-spec-utils.sh code-to-name 268437552



Pass the `-v` flag to print additonal info for version parts, which
is useful to debug issues.
HELP_EOF

}

main() {

    local return_value

    if [ "$1" = "-h" ] || [ "$1" = "--help" ]; then
        show_help || return $?
        return 0
    else
        if [ "${1:-}" = "-v" ]; then
            AASSU__LOG_LEVEL=2
            shift
        fi

        if [ "$1" = "name-to-code" ]; then
            shift
            name_to_code "$@"
            return_value=$?
        elif [ "$1" = "code-to-name" ]; then
            shift
            code_to_name "$@"
            return_value=$?
        else
            echo "Invalid command '$1'." 1>&2
            return_value=64
        fi
        if [ $return_value -eq 64 ]; then # EX__USAGE
            echo ""
            show_help
        fi
        return $return_value
    fi

}

name_to_code() {

    if [ $# -lt 1 ] || [ $# -gt 3 ]; then
        echo "Invalid argument count $#. The \"name_to_code\" method expects 1 to 3 arguments." 1>&2
        printf 'Arguments: %s\n' "$*" 1>&2
        return 64
    fi

    local version_name="${1/v/}"
    local release_variant="${2:-$AASSU__DEFAULT_RELEASE_VARIANT}"
    local noop="${3:-$AASSU__DEFAULT_NOOP}"

    local version_name_regex='^([0-9]|[1-5][0-9]|6[0-3])\.([0-9]|[1-9][0-9]|[1-4][0-9][0-9]|50[0-9]|51[0-1])\.([0-9]|[1-2][0-9]|3[0-1])(-(alpha|beta|rc)\.([1-9]|1[0-5]))?$'

    if ! printf "%s" "$version_name" | grep -qE "$version_name_regex"; then
        echo "The version_name '$version_name' is not valid as per android-app-semver spec compliant with semantic version '2.0.0' and  in the format 'major.minor.patch(-(alpha|beta|rc).<revision>)'. https://semver.org/spec/v2.0.0.html." 1>&2
        return 64
    fi

    if [[ ! "$release_variant" =~ ^[0-7]$ ]]; then
        echo "The release_variant '$release_variant' for version_name '$version_name' is not valid and must be between 0-7 (inclusive)." 1>&2
        return 64
    fi

    if [[ ! "$noop" =~ ^[0-1]$ ]]; then
        echo "The noop '$noop' for version_name '$version_name' is not valid and must be between 0-1 (inclusive)." 1>&2
        return 64
    fi


    local major
    local minor
    local patch
    local release_type
    local release_type_name
    local revision
    local version_code
    local version_code_binary

    major="$(printf "%s" "$version_name" | sed -E "s/$version_name_regex/\1/")" || return $?
    minor="$(printf "%s" "$version_name" | sed -E "s/$version_name_regex/\2/")" || return $?
    patch="$(printf "%s" "$version_name" | sed -E "s/$version_name_regex/\3/")" || return $?
    release_type_name="$(printf "%s" "$version_name" | sed -E "s/$version_name_regex/\5/")" || return $?
    revision="$(printf "%s" "$version_name" | sed -E "s/$version_name_regex/\6/")" || return $?

    case "$release_type_name" in
        alpha) release_type=0;;
        beta) release_type=1;;
        rc) release_type=2;;
        *) release_type_name="stable"; release_type=3; revision=0;;
    esac

    if [[ "$AASSU__LOG_LEVEL" == "2" ]]; then
        print_verbose_version_info || return $?
    fi

    if [ $release_type -lt 3 ] && [ $revision -lt 1 ]; then
        echo "The revision '$revision' for version_name '$version_name' is not valid and must not be 0 for a non-stable release with type '$release_type_name'." 1>&2
        return 1
    fi


    version_code=0
    version_code=$(( version_code + revision )) || return $?                         # 0 000 000000 000000000 00000 00 1111
    version_code=$(( version_code + (release_type << 4) )) || return $?              # 0 000 000000 000000000 00000 11 0000
    version_code=$(( version_code + (patch << 6) )) || return $?                     # 0 000 000000 000000000 11111 00 0000
    version_code=$(( version_code + (minor << 11) )) || return $?                    # 0 000 000000 111111111 00000 00 0000
    version_code=$(( version_code + (major << 20) )) || return $?                    # 0 000 111111 000000000 00000 00 0000
    version_code=$(( version_code + (release_variant << 26) )) || return $?          # 0 111 000000 000000000 00000 00 0000
    version_code=$(( version_code + (noop << 29) )) || return $?                     # 1 000 000000 000000000 00000 00 0000

    if [ $version_code -lt 0 ] || [ $version_code -gt $AASSU__MAX_VERSION_CODE ]; then
        echo "The version_code '$version_code' for version_name '$version_name' is not valid and must be between 0-$AASSU__MAX_VERSION_CODE (inclusive)." 1>&2
        return 1
    fi


    version_code_binary="$(decimal_to_binary "$version_code" 30)" || return $?
    version_code_binary="$(printf "%s\n" "$version_code_binary" | sed -E 's/(.{1})(.{3})(.{6})(.{9})(.{5})(.{2})(.{4})/\1 \2 \3 \4 \5 \6 \7/')" || return $?


    print_formatted_version_info || return $?

}


code_to_name() {

    if [ $# -ne 1 ]; then
        echo "Invalid argument count $#. The \"name_to_code\" method expects 1 argument." 1>&2
        printf 'Arguments: %s\n' "$*" 1>&2
        return 64
    fi

    local version_code="$1"

    if [[ ! "$version_code" =~ ^[0-9]+$ ]] || [ "$version_code" -gt $AASSU__MAX_VERSION_CODE ]; then
        echo "The version_code '$version_code' is not valid and must be between 0-$AASSU__MAX_VERSION_CODE (inclusive)." 1>&2
        return 64
    fi

    version_code_binary="$(decimal_to_binary "$version_code" 30)" || return $?
    version_code_binary="$(printf "%s\n" "$version_code_binary" | sed -E 's/(.{1})(.{3})(.{6})(.{9})(.{5})(.{2})(.{4})/\1 \2 \3 \4 \5 \6 \7/')" || return $?

    if [[ "$AASSU__LOG_LEVEL" == "2" ]]; then
        echo "version_code_binary: $version_code_binary"
    fi


    local major
    local minor
    local patch
    local release_type
    local release_type_name
    local revision
    local version_name

    noop=$(( (version_code & 536870912) >> 29 )) || return $?                    # 1 000 000000 000000000 00000 00 0000
    release_variant=$(( (version_code & 469762048) >> 26 )) || return $?         # 0 111 000000 000000000 00000 00 0000
    major=$(( (version_code & 66060288) >> 20 )) || return $?                    # 0 000 111111 000000000 00000 00 0000
    minor=$(( (version_code & 1046528) >> 11 )) || return $?                     # 0 000 000000 111111111 00000 00 0000
    patch=$(( (version_code & 1984) >> 6 )) || return $?                         # 0 000 000000 000000000 11111 00 0000
    release_type=$(( (version_code & 48) >> 4 )) || return $?                    # 0 000 000000 000000000 00000 11 0000
    revision=$(( (version_code & 15) )) || return $?                             # 0 000 000000 000000000 00000 00 1111


    version_name="$major.$minor.$patch"
    case "$release_type" in
        0) release_type_name=alpha;;
        1) release_type_name=beta;;
        2) release_type_name=rc;;
        *) release_type_name=""
    esac

    if [[ "$AASSU__LOG_LEVEL" == "2" ]]; then
        print_verbose_version_info || return $?
    fi


    if [ -n "$release_type_name" ]; then
        if [ $revision -lt 1 ]; then
            echo "The revision '$revision' for version_code '$version_code' is not valid and must not be 0 for a non-stable release with type '$release_type_name'." 1>&2
            return 1
        fi

        version_name+="-$release_type_name.$revision"
    else
        if [ "$revision" -ne 0 ]; then
            echo "The revision '$revision' for version_code '$version_code' is not valid and must be 0 for a stable release." 1>&2
            return 64
        fi
    fi


    print_formatted_version_info || return $?

}

print_verbose_version_info() {

    echo "noop: $noop"
    echo "release_variant: $release_variant"
    echo "major: $major"
    echo "minor: $minor"
    echo "patch: $patch"
    echo "release_type: $release_type (${release_type_name:-stable})"
    echo "revision: $revision"
    echo

}

print_formatted_version_info() {

    printf "version_name: %17s, version_code: %10s (%s), release_variant: %1s" "$version_name" "$version_code" "$version_code_binary" "$release_variant" || return $?
    if [ "$noop" -gt 0 ]; then
        printf ", noop: %1s" "$noop" || return $?
    fi
    echo

}

decimal_to_binary() {

    local decimal="$1"
    local min_digits="$2"
    if [[ ! "$min_digits" =~ ^[0-9]+$ ]]; then
        min_digits=8
    fi

    local binary
    local binary_digits
    local zeros=""
    local zeros_required

    binary="$(printf "%s\n" "ibase=10;obase=2;$decimal" | bc)" || return $?

    binary_digits=${#binary}
    if [ "$binary_digits" -lt $min_digits ]; then
        zeros_required=$((min_digits - binary_digits))
        zeros=""
        for (( i = 0; i < "$zeros_required"; i++ )); do
            zeros="${zeros}0"
        done
    fi

    printf "%s\n" "$zeros$binary"

}



# If running in bash, run script logic, otherwise exit with usage error
if [ -n "${BASH_VERSION:-}" ]; then
    # If script is sourced, return with success, otherwise call main function
    # - https://stackoverflow.com/a/28776166/14686958
    # - https://stackoverflow.com/a/29835459/14686958
    if (return 0 2>/dev/null); then
        return 0 # EX__SUCCESS
    else
        main "$@"
        exit $?
    fi
else
    (echo "${0##*/} must be run with the bash shell."; exit 64) # EX__USAGE
fi

android-app-semver-spec-utils.sh.zip

@agnostic-apollo
Copy link
Member Author

I prefer the android-app-semver spec over the one suggested in initial post since its reversible and will likely use that, even if it has lesser versions if only 1 number is used for a specific release_variant. The final spec selected can be merged before the v0.119.0 stable release if more time is needed for review.

Another issue was that any pre-releases like v0.119.0-beta.1 we made, F-Droid would have detected them automatically and marked them as Suggested version to the user. I sent a pull to F-Droid that was merged a few hours ago at https://gitlab.com/fdroid/fdroiddata/-/merge_requests/15199 and now the 0.118.1 and 0.119.0-beta.1 releases can be made. I already pulled the current master branch into a separate local branch and fixed it a few days ago to be released as v0.119.0-beta.1. The checking of bootstraps (including postint script execution) and runtime tests remain now.

I also have decided not to use app version names in packages to run logic, it should instead be based on environment variables, as Termux app and plugin forks and apps using termux-core library will have different versions than upstream Termux app, and they may merge multiple plugin apps into a single app, or only enable/integrate specific features depending on what they want or is possible as per different app store policies and exemptions they get. Instead different Termux APIs like env, core, termux-api will have respective $TERMUX_*__API_VERSION_CODE environment variable for the current API version code which will get incremented when changes/features are added to the API. Additionally, $TERMUX_*__FEATURES environment variables for the delimiter separated features the app or plugins support, like run command, content providers, permissions like storage, termux-api APIs, etc and their versions. Both variables can be checked at runtime before running specific logics if required. Some APIs also have their own API specific variables too, but backward compatibility is an issue, which will likely not be kept after 1.0.0 release.

agnostic-apollo added a commit that referenced this issue Jun 17, 2024
The `versionCode` has been bumped to `1000` so that users who have installed from F-Droid or GitHub should not have the app attempted to be updated by Google PlayStore and failing and also shown in PlayStore app updates list due to non-collaborative `v0.120` app release on PlayStore that set the `versionCode` higher than the latest F-Droid or GitHub `118` release. Unlike F-Droid, PlayStore does not check for difference in app APK signature before attempting to download and then failing to install due to signature mismatch.

- #4000
- #4012
agnostic-apollo added a commit that referenced this issue Jun 17, 2024
The `versionCode` has been bumped to `1020` so that users who have installed from F-Droid or GitHub should not have the app attempted to be updated by Google PlayStore and failing and also shown in PlayStore app updates list due to non-collaborative `v0.120` app release on PlayStore that set the `versionCode` higher than the latest F-Droid or GitHub `118` release. Unlike F-Droid, PlayStore does not check for difference in app APK signature before attempting to download and then failing to install due to signature mismatch.

The `v0.118.1` was released under `versionCode` `1000`, we bump `versionCode` to `1020` so that there are `20` version codes in between that can be used as patch releases for `0.118.x` in case needed, like for `v0.118.2`.

- #4000
- #4012
agnostic-apollo added a commit to termux/termux-boot that referenced this issue Jun 22, 2024
The `versionCode` has been bumped to `1000` so that users who have installed from F-Droid or GitHub should not have the app attempted to be updated by Google PlayStore and failing and also shown in PlayStore app updates list due to non-collaborative `v0.120` app release on PlayStore that set the `versionCode` higher than the latest F-Droid or GitHub `8` release. Unlike F-Droid, PlayStore does not check for difference in app APK signature before attempting to download and then failing to install due to signature mismatch.

- termux/termux-app#4000
- termux/termux-app#4012
- termux-play-store/termux-apps@9c10607
agnostic-apollo added a commit to termux/termux-styling that referenced this issue Jun 22, 2024
The `versionCode` has been bumped to `1000` so that users who have installed from F-Droid or GitHub should not have the app attempted to be updated by Google PlayStore and failing and also shown in PlayStore app updates list due to non-collaborative `v0.33` app release on PlayStore that set the `versionCode` higher than the latest F-Droid or GitHub `32` release. Unlike F-Droid, PlayStore does not check for difference in app APK signature before attempting to download and then failing to install due to signature mismatch.

- termux/termux-app#4000
- termux/termux-app#4012
- termux-play-store/termux-apps@b47c83d
tmzullinger added a commit to tmzullinger/termux-widget that referenced this issue Jun 25, 2024
The `versionCode` has been bumped to `1000` so that users who have installed from F-Droid or GitHub should not have the app attempted to be updated by Google PlayStore and failing and also shown in PlayStore app updates list due to non-collaborative `v0.14` app release on PlayStore that set the `versionCode` higher than the latest F-Droid or GitHub `13` release. Unlike F-Droid, PlayStore does not check for difference in app APK signature before attempting to download and then failing to install due to signature mismatch.

- termux/termux-app#4000
- termux/termux-app#4012
- termux-play-store/termux-apps@e51f8d2#diff-afedc8b49d567d2de57705052be9f6521366702c15f6ea32b2ae45174cef8221
@Sanzium
Copy link

Sanzium commented Sep 9, 2024

The GitHub Actions build still uses the 118 versionCode, Google Play tries to update it. Would it be possible to set a specific versionCode just for the automated workflows? Maybe set it a very high number, as it is like a nightly build.

@termux termux deleted a comment from Rendam180 Sep 9, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants