#!/usr/bin/env bash # # Everything the android CI job does inside the toolchain image, which is everything that needs a # glibc host. See build/android-build.Dockerfile for why there is an image at all, and # docs/platform-flags.md for the three rounds of CI that established it. # # It runs against a copy of the checkout at /build, put there with `docker cp` rather than a bind # mount — see the job for why — so it may read .git and it may write anywhere. What it leaves in # artifacts/android-nightly is what the job copies back out and publishes. # # Nothing here holds a secret. The key this signs with is committed to this repository in the open and # is meant to be; the token that publishes the result never enters this container. set -euo pipefail project=src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj staged=artifacts/android-nightly # Aapt2ToolPath takes the directory and aapt2 itself is inside it. Both come from the Android SDK in # this image rather than from the copy inside the workload pack: both work here, and using the SDK's # makes the build and the versionName read below the same binary, so the manifest the feed publishes # is read by the thing that wrote it. build_tools="$ANDROID_HOME/build-tools/$ANDROID_BUILD_TOOLS" echo "==> restore" # Locked mode, the same gate the solution restore gives every other project. This head is not in # DodoSSH.slnx — it needs a workload the other jobs have no reason to install — so this is the only # place its lock file is ever checked. It has silently gone stale before. # # Restored without a channel and built with one, which is safe and worth one line: the two channel # PropertyGroups change the application id, the version and some assembly metadata, and none of those # is in the restore graph. If a channel ever gains a PackageReference of its own, this splits and the # restore has to move under the same -p: as the build. # # Into /root/.nuget/packages, which the job mounts as a named volume — see the workflow. That is the # whole of this job's package caching and it is what took a run from eight minutes to two and a half. dotnet restore "$project" --locked-mode echo "==> package the nightly" # ◆ ONE BUILD, WHERE THERE WERE TWO, AND THE FIRST WAS NOT A CHECK. # # There used to be a plain `dotnet build` above this. It looked like a cheap compile before the # expensive packaging and it was neither: SignAndroidPackage depends on Build, so this line compiles # everything anyway, and the build above ran with no -p:DodoChannel — which means it ran as the # *release* channel. Different ApplicationId, different ApplicationVersion, different assembly # metadata; MSBuild treats a different set of global properties as a different project instance, so # not one output was reused. It was a complete second compile of the whole reference closure, # producing an APK for a channel this job must never build, thrown away unread. # # Removing it does not lose a check. Anything that fails in a compile fails in this one, and the # link-time failures below are the ones only packaging finds. # # Measured in the toolchain image with a warm package volume, same commit, same APK out of both: # # two builds 2m52 + 2m26 5m21 total # one build 3m01 3m04 total # # versionCode 203, versionName 0.0.0-alpha.0.136, dev.dodotech.dodossh.nightly, either way. # # Packaging rather than only compiling, because the two failures this head is most exposed to are both # link-time: a native library with no android ABI, and a managed assembly that resolves for net10.0 but # has nothing to dex. Neither shows up in a compile. # # versionCode is the commit count: monotonic by construction, and nobody has to remember anything. It # is not a version and is never displayed. versionName carries MinVer's full answer including the # prerelease height, which is what tells two nightlies apart. # # No RuntimeIdentifier, where this once pinned android-arm64. That produced the smallest possible build # check and the least installable artefact — an arm64-only APK will not run on an x86_64 emulator, # which is what most people testing a nightly actually have. code="$(git rev-list --count HEAD)" dotnet build "$project" \ --no-restore --configuration Release \ -t:SignAndroidPackage \ -p:DodoChannel=nightly \ -p:DodoNightlyVersionCode="$code" \ "-p:Aapt2ToolPath=$build_tools" apk="$(find src/DodoSSH.Client.Android/bin/Release -name '*-Signed.apk' | head -1)" if [ -z "$apk" ]; then echo "The package step produced no signed APK." >&2 exit 1 fi # Read back out of the APK rather than recomputed, so what the feed advertises is what the bytes say. # A versionName derived a second time in shell is a second implementation of the csproj's target, and # the two would drift on the first change to either. badging="$("$build_tools/aapt2" dump badging "$apk")" name="$(printf '%s' "$badging" | sed -n "s/.*versionName='\([^']*\)'.*/\1/p" | head -1)" if [ -z "$name" ]; then echo "aapt2 reported no versionName for $apk." >&2 printf '%s\n' "$badging" | head -3 >&2 exit 1 fi mkdir -p "$staged" cp "$apk" "$staged/DodoSSH-nightly-$name.apk" # The channel manifest, which is what the client reads and the whole reason the feed is machine- # readable at all. versionCode is the comparison — it is the number Android itself uses to accept or # refuse an install, so comparing anything else would let the client offer an update the platform then # rejects. versionName is for the person reading the banner. printf '{"versionCode":%s,"versionName":"%s","apk":"DodoSSH-nightly-%s.apk"}' \ "$code" "$name" "$name" > "$staged/android-nightly.json" printf '%s\n' "$badging" | head -1 ls -la "$staged"