Public Access
The Android head had no updater and no release path, and the two are one problem: Android refuses an update signed by a different key, and CI generates a fresh debug key in every container. An APK released from a workflow could be installed once and never updated again — each new one an uninstall, which on this product means losing the cache, the outbox and the device key. So there are two channels, and they are two applications because the platform gives no third option. dev.dodotech.dodossh is cut from a v* tag by a person running scripts/release-android.ps1 with the key ADR 0011 rule 1 keeps off runners. dev.dodotech.dodossh.nightly is cut from main by CI and signed with a keystore committed here in the open — a key everybody has cannot be stolen and grants nothing by being held, which is why putting it in CI does not touch the rule. Neither can update the other, by construction. See ADR 0014. The android job assumed an image with a JDK and an Android SDK on it, which is what a GitHub runner is and what this project's is not. It now installs a JDK, fetches Google's command-line tools, accepts the licences and installs API 36 — each a no-op where it is already satisfied, and each cached by the persistent runner's own disk rather than by an action that would move a quarter of a gigabyte to rebuild a directory that never left. The client reads a small JSON manifest beside the APK, the counterpart of releases.win.json, and compares Android's versionCode rather than a version name: that integer is what the platform itself uses to accept or refuse an install, so comparing anything else would offer updates the phone then rejects. It fetches, and then asks Android to ask — the system draws its own confirmation, and from API 26 will not draw even that until unknown sources is on for this application. IUpdateChannel gained ApplyingEndsTheProcess. On Windows applying replaces the files and restarts, so the shell disposes the vault first and that is what zeroes the keys. On the phone the install is a request and the answer may be no, so disposing first would answer "not now" with a locked keychain and every shell closed — a punishment for declining an update. Two measured bugs found on the way, both older than this work and both invisible to a -getProperty check. ApplicationDisplayVersion is read by the Android targets in a top-level PropertyGroup, so the target setting it from MinVer ran after the only thing that reads it: every APK ever built here said versionName 1.0.0. And nothing found so far varies the launcher name per channel — four mechanisms tried, all of them recorded in platform-flags, none of them reaching the label the launcher shows. The two channels share an icon name for now and are told apart by package name, version, and what the preferences screen says.
203 lines
12 KiB
XML
203 lines
12 KiB
XML
<Project Sdk="Microsoft.NET.Sdk">
|
|
|
|
<PropertyGroup>
|
|
<TargetFramework>net10.0-android</TargetFramework>
|
|
<OutputType>Exe</OutputType>
|
|
<Nullable>enable</Nullable>
|
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
|
|
|
<!--
|
|
minSdk 28 (Android 9), targetSdk 36. Chosen rather than inherited, because docs/android-port.md
|
|
flagged it as a real decision: the packages constrain targetSdk, not the floor.
|
|
|
|
28 is where the three platform APIs this head actually depends on all exist in the framework
|
|
itself rather than behind an AndroidX shim: BiometricPrompt, StrongBox-backed keys, and
|
|
KeyGenParameterSpec.SetUnlockedDeviceRequired. Going lower would mean carrying androidx.biometric
|
|
to reach devices that mostly cannot hold a hardware-backed key anyway — which is the one thing
|
|
the device key store is for.
|
|
-->
|
|
<SupportedOSPlatformVersion>28</SupportedOSPlatformVersion>
|
|
<TargetPlatformVersion>36</TargetPlatformVersion>
|
|
|
|
<!--
|
|
============ WHICH CHANNEL THIS BUILD BELONGS TO ============
|
|
|
|
Two of them, and they are two applications rather than two configurations. Android identifies an
|
|
app by its package id and its signing certificate together, and refuses an update signed by a
|
|
different key — so a build CI signs and a build a person signs cannot replace one another whatever
|
|
they are called. Pretending otherwise would ship an installer that fails with
|
|
INSTALL_FAILED_UPDATE_INCOMPATIBLE at the one moment somebody is trying to get a fix.
|
|
|
|
So each channel gets its own id, its own key and its own feed, and neither can update the other by
|
|
construction rather than by accident. Both can be installed at once, which is the useful half: a
|
|
person testing a nightly does not lose the release they rely on. See ADR 0014.
|
|
|
|
release — dev.dodotech.dodossh, signed by the key ADR 0011 puts on a machine that is not a runner.
|
|
Cut from a v* tag by scripts/release-android.ps1, by a person.
|
|
nightly — dev.dodotech.dodossh.nightly, signed by a keystore committed to this repository in the
|
|
open. Cut from main by CI, every push.
|
|
|
|
Default release, so that an unqualified `dotnet build` is the real application and the nightly is
|
|
the one you have to ask for. A build with no keystore named at all falls back to the debug key,
|
|
which is what a developer's own device gets and is neither channel.
|
|
-->
|
|
<DodoChannel Condition="'$(DodoChannel)' == ''">release</DodoChannel>
|
|
|
|
<!--
|
|
False here for the same reason the desktop head sets it false: this process formats timestamps
|
|
and host names for a person. See DodoSSH.Client.App.csproj.
|
|
-->
|
|
<InvariantGlobalization>false</InvariantGlobalization>
|
|
</PropertyGroup>
|
|
|
|
<!-- ============ the release channel ============ -->
|
|
<PropertyGroup Condition="'$(DodoChannel)' == 'release'">
|
|
<ApplicationId>dev.dodotech.dodossh</ApplicationId>
|
|
|
|
<!--
|
|
versionCode, and it stays a hand-bumped literal on this channel. Android requires a monotonically
|
|
increasing integer and SemVer does not give you one — deriving it from the version would work until
|
|
the day a patch number reached 10 and the arithmetic went backwards, which is the sort of failure
|
|
that surfaces as an install the phone refuses for reasons it will not explain.
|
|
|
|
Forgetting to bump it is caught rather than shipped: scripts/release-android.ps1 reads the code
|
|
already published to the release channel and refuses to build one that is not higher. A guard on a
|
|
literal, which is the arrangement this comment argued for, rather than arithmetic nobody checks.
|
|
-->
|
|
<ApplicationVersion>1</ApplicationVersion>
|
|
</PropertyGroup>
|
|
|
|
<!-- ============ the nightly channel ============ -->
|
|
<PropertyGroup Condition="'$(DodoChannel)' == 'nightly'">
|
|
<ApplicationId>dev.dodotech.dodossh.nightly</ApplicationId>
|
|
|
|
<!--
|
|
Passed in by CI as the number of commits on the branch, which is monotonic by construction and needs
|
|
nobody to remember anything. It is meaningless as a version and is not shown anywhere: versionName
|
|
is what a person reads, and it carries MinVer's full answer on this channel including the height —
|
|
see the target below.
|
|
|
|
The fallback exists so that `-p:DodoChannel=nightly` builds outside CI at all, and 1 is deliberately
|
|
the lowest it can be: a locally built nightly then cannot replace one installed from the feed, which
|
|
is the right way round.
|
|
-->
|
|
<ApplicationVersion Condition="'$(DodoNightlyVersionCode)' != ''">$(DodoNightlyVersionCode)</ApplicationVersion>
|
|
<ApplicationVersion Condition="'$(DodoNightlyVersionCode)' == ''">1</ApplicationVersion>
|
|
|
|
<!--
|
|
◆ THE KEY IS IN THE REPOSITORY, IN THE OPEN, AND THAT IS THE DESIGN RATHER THAN AN OVERSIGHT.
|
|
|
|
ADR 0011 rule 1 says the release key is never in CI, because a key a workflow can reach is a key held
|
|
by everyone who can change a workflow file. This does not break that rule; it sidesteps it by making
|
|
the CI key worth nothing. Everybody has it, so holding it grants nothing, so there is nothing to
|
|
steal and no secret to configure — which is also why this channel works on a fresh runner with no
|
|
setup at all.
|
|
|
|
What it costs is stated rather than buried: anyone can build an APK this channel's phones will accept
|
|
as an update. Reaching one still means being the thing they fetch from, which is a release on
|
|
git.dodotech.cloud over TLS — so the practical set is whoever can write a release here, the same set
|
|
ADR 0013 names for the desktop. That is an acceptable trade for a nightly and is not one for the
|
|
release channel, which is the whole reason there are two.
|
|
-->
|
|
<AndroidKeyStore>true</AndroidKeyStore>
|
|
<AndroidSigningKeyStore>$(MSBuildThisFileDirectory)../../build/nightly.keystore</AndroidSigningKeyStore>
|
|
<AndroidSigningKeyAlias>dodossh-nightly</AndroidSigningKeyAlias>
|
|
<AndroidSigningStorePass>nightly</AndroidSigningStorePass>
|
|
<AndroidSigningKeyPass>nightly</AndroidSigningKeyPass>
|
|
</PropertyGroup>
|
|
|
|
<ItemGroup>
|
|
<!--
|
|
Which feed this build asks. Assembly metadata rather than a compile-time constant, because the
|
|
updater needs the string and not a branch — and because a value baked into the assembly is one a
|
|
crash report can be asked for. See AndroidUpdateChannel.
|
|
-->
|
|
<AssemblyMetadata Include="DodoChannel" Value="$(DodoChannel)" />
|
|
</ItemGroup>
|
|
|
|
<!--
|
|
versionName, which is the string a person sees in Android's app info, taken from the same v* tag
|
|
as everything else so the two heads cannot claim different versions of one product.
|
|
|
|
It has to be a target rather than a property, and that is the whole reason this is nine lines
|
|
instead of one. MinVer computes the version in a target of its own, so at evaluation time $(Version)
|
|
is still the SDK's default 1.0.0 — an ApplicationDisplayVersion set in a PropertyGroup would read
|
|
as though it were derived and would ship 1.0.0 forever. Running after MinVer is what makes it true.
|
|
|
|
Major.Minor.Patch on the release channel, without MinVer's prerelease or build-metadata parts:
|
|
versionName is a free string to Android, but it is shown to users, and "0.2.0-alpha.0.7+1a2b3c4" is
|
|
not a version anybody can read back to you over a support conversation.
|
|
|
|
The nightly channel keeps the whole thing, prerelease height and all, and for the opposite reason.
|
|
Every nightly between two tags shares one Major.Minor.Patch, so trimming it would put the same three
|
|
numbers on every build for weeks — on the one channel where "which build is this?" is the entire
|
|
question being asked. Nobody reads a nightly's version aloud; they paste it.
|
|
-->
|
|
<!--
|
|
◆ BeforeTargets on the stub generator, and it assigns an underscore-prefixed property. Both are
|
|
deliberate, and the shape this replaced looked more correct while shipping 1.0.0 on every build.
|
|
|
|
ApplicationDisplayVersion is read by Xamarin.Android.Common.targets in a plain top-level
|
|
PropertyGroup — `<_AndroidVersionName>$(ApplicationDisplayVersion)</_AndroidVersionName>` — which is
|
|
*evaluation*, not a target. Every project property is already final by the time any target runs, so a
|
|
target that sets ApplicationDisplayVersion sets it after the only thing that reads it has finished.
|
|
MinVer cannot run at evaluation time, so there is no arrangement of the public property that works.
|
|
|
|
Measured rather than reasoned about: aapt2 dump badging on the packaged APK said versionName='1.0.0'
|
|
while `-getProperty:ApplicationDisplayVersion` said 0.0.0-alpha.0.124. The property was right and the
|
|
manifest was not, which is exactly the shape of bug a -getProperty check cannot catch.
|
|
|
|
So this reaches for _AndroidVersionName, which is the value GenerateMainAndroidManifest is actually
|
|
handed, at the last moment before _GenerateJavaStubs runs it. An internal name is a real cost and it
|
|
is the smaller one: the alternative is passing -p:ApplicationDisplayVersion from CI and from the
|
|
release script, which makes an ordinary `dotnet build` of this head go on lying about its version and
|
|
puts the number in two places that can disagree.
|
|
|
|
ApplicationDisplayVersion is still assigned, and first, so that a caller passing it as a global
|
|
property wins — a global beats a target assignment, and _AndroidVersionName then follows theirs.
|
|
-->
|
|
<Target Name="UseTheDerivedVersionForAndroid"
|
|
BeforeTargets="_GenerateJavaStubs"
|
|
DependsOnTargets="MinVer">
|
|
<PropertyGroup>
|
|
<!--
|
|
Guarded, because the failure without it is silent and absurd: MinVerMajor and its two siblings
|
|
are empty until MinVer has run, so an unguarded assignment yields the versionName ".." — three
|
|
characters that are a legal Android versionName and are what the phone would then show. Measured,
|
|
not imagined; invoking this target on its own produced exactly that.
|
|
-->
|
|
<ApplicationDisplayVersion Condition="'$(MinVerMajor)' != '' and '$(DodoChannel)' != 'nightly'">$(MinVerMajor).$(MinVerMinor).$(MinVerPatch)</ApplicationDisplayVersion>
|
|
<ApplicationDisplayVersion Condition="'$(MinVerVersion)' != '' and '$(DodoChannel)' == 'nightly'">$(MinVerVersion)</ApplicationDisplayVersion>
|
|
|
|
<_AndroidVersionName Condition="'$(ApplicationDisplayVersion)' != ''">$(ApplicationDisplayVersion)</_AndroidVersionName>
|
|
</PropertyGroup>
|
|
</Target>
|
|
|
|
<ItemGroup>
|
|
<!-- The phone's control styles. The palette they draw from lives in Shell and is shared. -->
|
|
<AvaloniaResource Include="Theme/**" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<PackageReference Include="Avalonia" />
|
|
<PackageReference Include="Avalonia.Android" />
|
|
<PackageReference Include="Avalonia.Themes.Fluent" />
|
|
<PackageReference Include="Avalonia.Fonts.Inter" />
|
|
<PackageReference Include="Avalonia.Controls.WebView" />
|
|
<PackageReference Include="CommunityToolkit.Mvvm" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<!--
|
|
The same view models the desktop head drives. That sharing is the whole reason this head is a
|
|
sibling project rather than a fork: docs/android-port.md's phone-first decision says the interface
|
|
is a redesign, and that what survives it is every view model, command and piece of state.
|
|
-->
|
|
<ProjectReference Include="../DodoSSH.Client.Shell/DodoSSH.Client.Shell.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Session/DodoSSH.Client.Session.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Ssh/DodoSSH.Client.Ssh.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Terminal/DodoSSH.Client.Terminal.csproj" />
|
|
</ItemGroup>
|
|
|
|
</Project>
|