diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4dfee39..20e48ae 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -394,6 +394,10 @@ jobs: # versioned separately and this one is the current stable. Floating it would make the toolchain # a moving part of every build. CMDLINE_TOOLS: commandlinetools-linux-11076708_latest.zip + # Named once here because three things now depend on it being one version: what sdkmanager + # installs, what the build is told to use for aapt2, and what the packaging step reads the + # versionName back with. + BUILD_TOOLS: 36.0.0 run: | set -eu SUDO="" @@ -448,22 +452,83 @@ jobs: # terminal. See docs/android-port.md. platform-tools comes along because aapt2 and apksigner # are what the packaging step actually shells out to. "$sdk/cmdline-tools/latest/bin/sdkmanager" \ - "platform-tools" "platforms;android-36" "build-tools;36.0.0" + "platform-tools" "platforms;android-36" "build-tools;$BUILD_TOOLS" + # ============ aapt2, which the build is told to take from here and not from the workload ============ + # + # .NET for Android ships its own aapt2 inside the workload pack, at + # packs/Microsoft.Android.Sdk.Linux//tools/Linux/aapt2, and prefers it when Aapt2ToolPath is + # unset. On this runner that binary could not be started at all: + # + # warning : An error occurred trying to start process '.../tools/Linux/aapt2' … No such file + # error XA0111: Unsupported version of AAPT2 found at path '.../tools/Linux' + # + # The second line is what the build reports and it is misleading — nothing was found, so nothing + # had a version. This runner is persistent (act's host executor, so /usr/share/dotnet survives + # between runs), and `dotnet workload install` treats an already-listed workload as satisfied + # whatever is actually on disk, so a pack left incomplete by an interrupted install stays + # incomplete for every later run. That is a state no step here can detect and none can repair. + # + # So the build is pointed at the aapt2 from build-tools instead: Google's own, installed by the + # line above, in a directory this job creates and can therefore vouch for. It is also already + # the aapt2 the packaging step shells out to for `dump badging`, so this makes one tool of what + # were two — and the manifest the feed publishes is now read by the same binary that wrote it. + # + # Verified rather than assumed to be usable, because the failure mode above is precisely a file + # that exists and will not run, and because a version check here says so in one line instead of + # as an XA0111 four minutes into a build. + aapt2="$sdk/build-tools/$BUILD_TOOLS" + + if ! "$aapt2/aapt2" version; then + echo "aapt2 at $aapt2 will not run, so there is no usable one on this runner." >&2 + exit 1 + fi + + echo "AAPT2_TOOL_PATH=$aapt2" >> "$GITHUB_ENV" echo "Android SDK at $sdk" # After the SDK, because the workload's own first-run checks look for one and are quieter when they # find it. --skip-sign-check is for the workload package feed, not for anything this project signs. + # + # The repair is here because `install` is not one. It reads the installed-workload records and does + # nothing when android is listed, whatever is on disk — so a pack left half-extracted by an + # interrupted run stays half-extracted forever on a runner whose filesystem persists, and every + # later build fails on whichever file happened not to make it. This runner has already produced + # exactly that, with a missing tools/Linux/aapt2 (see the SDK step above). + # + # tools/Linux/aapt2 is the probe rather than the point: the build no longer uses that binary at all. + # It is a 5 MB file near the end of a 130 MB package, which makes it a good witness for a truncated + # extraction — and if it is absent then r8.jar and manifestmerger.jar, which the build does need, + # are the next things to go. Repairing costs a re-download and only happens when something is + # already wrong; the alternative is a mystery every few months. - name: install the android workload - run: dotnet workload install android --skip-sign-check + run: | + set -eu + + dotnet workload install android --skip-sign-check + + root="$(dirname "$(readlink -f "$(command -v dotnet)")")" + + # tools/Linux/ and not tools/, because the pack's layout is host-shaped: the Linux pack puts its + # host binaries under a named directory and the Windows one puts aapt2.exe straight in tools/. + # This job is linux-only, so the Linux path is the one to look for and a probe that accepted + # either would quietly pass on a pack for the wrong host. + if [ -d "$root/packs" ] && ! ls "$root"/packs/Microsoft.Android.Sdk.*/*/tools/Linux/aapt2 >/dev/null 2>&1; then + echo "::warning::The android workload pack is missing files; repairing it." + dotnet workload repair + fi - name: restore run: dotnet restore src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj --locked-mode + # Aapt2ToolPath on both this and the packaging step, not on one: it is a directory the workload + # would otherwise pick for itself, and a build that resolved a different aapt2 than the package + # step would be a difference nobody could see until one of them failed. See the SDK step. - name: build run: > dotnet build src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj --no-restore --configuration Release + -p:Aapt2ToolPath="$AAPT2_TOOL_PATH" # 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 @@ -494,7 +559,8 @@ jobs: --no-restore --configuration Release \ -t:SignAndroidPackage \ -p:DodoChannel=nightly \ - -p:DodoNightlyVersionCode="$code" + -p:DodoNightlyVersionCode="$code" \ + -p:Aapt2ToolPath="$AAPT2_TOOL_PATH" apk="$(find src/DodoSSH.Client.Android/bin/Release -name '*-Signed.apk' | head -1)" if [ -z "$apk" ]; then @@ -505,7 +571,7 @@ jobs: # 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="$($ANDROID_HOME/build-tools/36.0.0/aapt2 dump badging "$apk")" + badging="$("$AAPT2_TOOL_PATH/aapt2" dump badging "$apk")" name="$(printf '%s' "$badging" | sed -n "s/.*versionName='\([^']*\)'.*/\1/p" | head -1)" staged="$RUNNER_TEMP/android-nightly" diff --git a/docs/platform-flags.md b/docs/platform-flags.md index 40bc932..7814777 100644 --- a/docs/platform-flags.md +++ b/docs/platform-flags.md @@ -574,6 +574,37 @@ The lasting hazard is the first paragraph and not the fix. Any change to a share to a lock file this repository cannot verify from a machine without the Android workload, and it will go on being noticed later than every other one. +**`dotnet workload install` is not a repair, and on a persistent runner that is the difference between a +one-off failure and a permanent one.** It reads the installed-workload records and does nothing when the +workload is listed, whatever is actually on disk. This runner is act's host executor — the checkout path +is `/root/.cache/act//hostexecutor`, so `/usr/share/dotnet` survives between runs — and it reached +a state where the android pack's targets were present and `tools/Linux/aapt2` was not. Every later run +reinstalled nothing and failed identically: + +``` +warning : An error occurred trying to start process '…/packs/Microsoft.Android.Sdk.Linux/36.1.69/tools/Linux/aapt2' … No such file or directory +error XA0111: Unsupported version of AAPT2 found at path '…/tools/Linux' +``` + +**The error names the wrong problem**, which is the part worth remembering: nothing was found, so nothing +had a version, and XA0111 sends you to `Aapt2ToolPath` in the project file — where there is nothing to +find. The warning above it is the real message and it is a warning. + +Two changes, and each stands on its own. The build is now given +`-p:Aapt2ToolPath=$ANDROID_HOME/build-tools/`, so it uses Google's aapt2 from the SDK the job installs +itself rather than the workload's copy — the same binary the packaging step already shells out to for +`dump badging`, which makes one tool of what were two and means the manifest the feed publishes is read +by the binary that wrote it. Verified compatible rather than assumed: build-tools 36.0.0 answers +`aapt2 2.20-13193326` and .NET for Android 36.1.43 builds and packages the head against it without +complaint. And the workload step now probes for that pack file and runs `dotnet workload repair` when it +is absent — the probe is a witness rather than the point, since a 5 MB file near the end of a 130 MB +package is what a truncated extraction loses first, and `r8.jar` and `manifestmerger.jar` are what it +loses next. + +The pack layout is host-shaped, which the probe has to know: the Linux pack keeps its host binaries under +`tools/Linux/`, the Windows one puts `aapt2.exe` straight in `tools/`. A probe written on Windows and +matching `tools/aapt2*` finds nothing on Linux and repairs a pack that is fine. + **A Docker `ARG` named `VERSION` silently sets MSBuild's `Version`.** An `ARG` is an environment variable for the rest of the stage, MSBuild reads environment variables as global properties, and MSBuild property names are case-insensitive — so `ARG VERSION` in a build stage sets `Version` for every project built in