Public Access
ReadyToRun went onto the win-x64 publish in the previous change and took the main window from 2.6 s to about 1.4 s on its own. The cause it answers — every launch JIT-compiling Avalonia, the XAML and EF Core's model building — is the same on every desktop, so the condition is now any RID rather than win-*. Verified in the Alpine SDK container that reproduces CI: osx-arm64 and linux-x64 both cross-compile, `vpk '[osx]' bundle` builds the .app with its Info.plist, and scripts/ci-linux-rpm.sh builds the RPM with its provides and requires checks passing. Each publish lists exactly the same files with ReadyToRun as without, because the precompiled code lives inside the managed assemblies — so the per-dylib signing loop in release-macos.sh and the RPM's library scan have nothing new to see. macOS runs it under the allow-jit entitlement the JIT already needed. The cost is size, as on Windows: osx-arm64 from 141 MB to 192 MB, linux-x64 from 130 MB to 172 MB. Not measured: the startup gain on either platform, which the csproj comment says rather than implies, and anything about the signed and notarized macOS build, which only release-macos.sh on a Mac produces. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
221 lines
12 KiB
XML
221 lines
12 KiB
XML
<Project Sdk="Microsoft.NET.Sdk">
|
|
|
|
<PropertyGroup>
|
|
<OutputType>WinExe</OutputType>
|
|
<ApplicationManifest>app.manifest</ApplicationManifest>
|
|
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
|
|
|
|
<!--
|
|
The project is named for its layer and the executable is named for the product. Users see the
|
|
executable — in Task Manager, in a shortcut's target, in the SmartScreen dialog an unsigned
|
|
installer raises — and DodoSSH.Client.App.exe reads as an implementation detail leaking out.
|
|
|
|
Safe to change now and not later: it becomes the main executable recorded in every Velopack
|
|
package, and changing it after a release means the updater looks for an executable that is no
|
|
longer there. Nothing else depends on the name — no avares://DodoSSH.Client.App URI exists, the
|
|
window icon is the assembly-relative /Assets/dodossh.ico, and WindowsDeviceKeyStore names its
|
|
CNG key with a literal.
|
|
-->
|
|
<AssemblyName>DodoSSH</AssemblyName>
|
|
|
|
<!--
|
|
No RuntimeIdentifiers here, and that is the considered answer rather than an omission.
|
|
|
|
Declaring win-x64 is the obvious way to let a self-contained publish restore, and it was tried:
|
|
it works, and it also writes a net10.0/win-x64 target into the lock file of every project this
|
|
one references transitively — which includes DodoSSH.Contracts and DodoSSH.Crypto, which the
|
|
server builds too. The API's Dockerfile then restores those projects with no RID and locked
|
|
mode, and fails NU1004 on a lock file that has grown a runtime identifier the server knows
|
|
nothing about. The desktop head's packaging would have broken the server's image build.
|
|
|
|
So the RID stays out of the committed state entirely, and the publish that needs one asks for
|
|
it unlocked — see scripts/release-windows.ps1 and the CI step that proves the same thing. The
|
|
locked restore that gates every ordinary build is untouched, which is the property worth
|
|
keeping. See docs/platform-flags.md.
|
|
-->
|
|
|
|
<!--
|
|
The icon on the executable itself — what Explorer, the Start menu and a pinned taskbar button
|
|
draw, all of which read it from the PE resource and never start the process. Window.Icon in
|
|
MainWindow.axaml is a separate thing that only exists once the application is running; both are
|
|
needed, and both point at this file.
|
|
-->
|
|
<ApplicationIcon>Assets/dodossh.ico</ApplicationIcon>
|
|
|
|
<!--
|
|
============ WHICH CHANNEL THIS BUILD BELONGS TO ============
|
|
|
|
The same split the Android head has, for the same reason and with one difference worth stating up
|
|
front: Android gets separation for free, because the platform refuses an update signed by a
|
|
different key, so its two channels cannot replace one another whatever anybody does. Nothing
|
|
refuses anything here. Velopack applies what its feed serves, so the separation has to be built:
|
|
two pack ids, two Velopack channels, two feeds, and a profile directory each.
|
|
|
|
release — DodoSSH.Desktop, Velopack channel win, read from the newest non-prerelease release.
|
|
Cut from a v* tag by scripts/release-windows.ps1, by a person. See ADR 0013 rule 3.
|
|
nightly — DodoSSH.Desktop.Nightly, Velopack channel win-nightly, read from a prerelease release
|
|
CI replaces on every push to main.
|
|
|
|
Default release, so an unqualified `dotnet build` is the real application and the nightly is the
|
|
one you have to ask for.
|
|
|
|
What this property does *not* decide is the pack id or the title. Those are arguments to vpk and
|
|
live where the packaging happens — in ci.yml for the nightly and in the release script for the
|
|
release. Putting them here would suggest the build knows which package it will end up inside,
|
|
and it does not.
|
|
-->
|
|
<DodoChannel Condition="'$(DodoChannel)' == ''">release</DodoChannel>
|
|
|
|
<!--
|
|
For the macOS keychain interop in Platform/, and for nothing else.
|
|
|
|
Set on this project rather than in Directory.Build.props deliberately. The frameworks that hold a
|
|
Secure Enclave key take CFDictionaries of raw pointers, so building one means pinning arrays and
|
|
taking their addresses — see MacDeviceKeyStore. Every other project here is managed code with no
|
|
business doing that, and a solution-wide flag would quietly permit it everywhere, including in the
|
|
crypto project where a stray pointer is the last thing anybody wants to have been allowed.
|
|
|
|
The alternative — GCHandle.Alloc with GCHandleType.Pinned — needs no flag and was considered. It
|
|
would replace each `fixed` with an allocate/free pair that has to be balanced by hand across the
|
|
early returns those methods are full of, which trades a compiler-checked scope for a manual one.
|
|
-->
|
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
|
|
|
<!--
|
|
False here, unlike every server project. The root Directory.Build.props sets it true because
|
|
the API is container-hosted, UTC-only and has no business formatting anything for a human.
|
|
This process does: it shows timestamps and host names to a person who expects their own
|
|
locale's conventions. Invariant globalization would quietly render every date in a format
|
|
that is correct for nobody.
|
|
-->
|
|
<InvariantGlobalization>false</InvariantGlobalization>
|
|
|
|
<!--
|
|
Precompiled, on every desktop publish, because a cold start here is mostly the JIT. The runtime
|
|
pack's own assemblies already ship precompiled; ours and every package's — Avalonia, Skia's
|
|
bindings, EF Core, SSH.NET — ship as IL, and before this every launch compiled the toolkit, the XAML
|
|
and EF's model building method by method before anything reached the screen.
|
|
|
|
Measured on a self-contained win-x64 publish, warm disk, median of six launches: the splash from
|
|
735 ms to 536 ms after the process starts, and the main window idle on its first screen from 2.6 s
|
|
to 1.4 s. The cost is size, because each method is carried as IL and as code: the publish grows from
|
|
146 MB to 175 MB and the Setup executable from 70 MB to 89 MB. That is the trade worth making for an
|
|
application opened many times a day and installed once — and it is a fifth of what dropping the
|
|
native symbols below saves, so the first install is still far smaller than it once was.
|
|
|
|
The numbers above are Windows's, and only Windows has been timed. macOS and Linux take the same
|
|
publish — osx-arm64 and linux-x64 are cross-compiled on the same Linux runner, and ReadyToRun code
|
|
lives inside the managed assemblies, so neither the per-dylib signing loop in release-macos.sh nor
|
|
the RPM's library scan sees a new file: both publishes list exactly the same files with it as
|
|
without. They grow the same way Windows's does, osx-arm64 from 141 MB to 192 MB and linux-x64 from
|
|
130 MB to 172 MB. On macOS it runs under the allow-jit entitlement the JIT already needs. Whether they gain as much is unmeasured; that they gain at all is the runtime's own
|
|
bet, since its framework assemblies ship precompiled for both.
|
|
|
|
Conditioned on the RID rather than set outright, so a plain `dotnet build`, `dotnet run` and every
|
|
test project referencing this one are untouched: ReadyToRun is a publish step and needs a RID to
|
|
target.
|
|
-->
|
|
<PublishReadyToRun Condition="'$(RuntimeIdentifier)' != ''">true</PublishReadyToRun>
|
|
</PropertyGroup>
|
|
|
|
<ItemGroup>
|
|
<!--
|
|
Named rather than globbed as Assets/**, because the folder also holds the script that draws the
|
|
icon and a build has no reason to carry a copy of it around inside the binary.
|
|
-->
|
|
<AvaloniaResource Include="Assets/dodossh.ico" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<!--
|
|
Which channel this build belongs to, carried in the assembly the same way the Android head carries
|
|
it. Metadata rather than a compile-time constant for the reason stated there: the updater needs the
|
|
string rather than a branch, and a value baked into the assembly is one a crash report can be asked
|
|
for. See DesktopChannel, which is the only thing that reads it.
|
|
-->
|
|
<AssemblyMetadata Include="DodoChannel" Value="$(DodoChannel)" />
|
|
</ItemGroup>
|
|
|
|
<!--
|
|
Two native symbol files, and they are the reason a self-contained publish weighed 227 MB.
|
|
|
|
Measured on a win-x64 publish: libSkiaSharp.pdb is 80.1 MB and libHarfBuzzSharp.pdb is 19.9 MB, so
|
|
the two of them are 100 MB of debug symbols for third-party native code nobody here will ever step
|
|
through. Dropping them takes the publish to about 127 MB, which is most of what a first install
|
|
costs somebody on a slow connection.
|
|
|
|
Our own symbols stay, and the distinction is the point rather than a compromise. All fifteen managed
|
|
PDBs together are 0.93 MB, and with them present an Exception.ToString() carries file names and line
|
|
numbers — which for a self-hosted product is the whole diagnostic channel, because the way a fault
|
|
gets reported is a user pasting a stack into an issue.
|
|
|
|
Named one by one instead of matched by a pattern. A rule like "drop every .pdb whose assembly is
|
|
native" would be shorter and would silently start dropping ours the day a managed library ships a
|
|
file this heuristic misreads, and a build that quietly stops carrying line numbers is not a thing
|
|
anybody notices until they need them.
|
|
-->
|
|
<Target Name="DropNativeSymbolsFromPublish" AfterTargets="ComputeResolvedFilesToPublishList">
|
|
<ItemGroup>
|
|
<ResolvedFileToPublish
|
|
Remove="@(ResolvedFileToPublish)"
|
|
Condition="'%(Filename)%(Extension)' == 'libSkiaSharp.pdb'
|
|
or '%(Filename)%(Extension)' == 'libHarfBuzzSharp.pdb'" />
|
|
</ItemGroup>
|
|
</Target>
|
|
|
|
<ItemGroup>
|
|
<PackageReference Include="Avalonia" />
|
|
<PackageReference Include="Avalonia.Desktop" />
|
|
<PackageReference Include="Avalonia.Themes.Fluent" />
|
|
<PackageReference Include="Avalonia.Fonts.Inter" />
|
|
<PackageReference Include="Avalonia.Controls.WebView" />
|
|
<PackageReference Include="CommunityToolkit.Mvvm" />
|
|
<!--
|
|
Here and in no other project. The Android head must never reference it — that head's
|
|
distribution is settled by ADR 0011 and has no updater — and DodoSSH.Client.Shell is shared
|
|
between the two heads, so the one file that names Velopack lives in Platform/ beside
|
|
WindowsDeviceKeyStore, which is the same shape of thing: a Windows-only implementation of an
|
|
interface declared in DodoSSH.Client.Session.
|
|
-->
|
|
<PackageReference Include="Velopack" />
|
|
|
|
<!--
|
|
Here for the same reason and with the same boundary as Velopack above: LinuxDeviceKeyStore is a
|
|
Linux-only implementation of an interface declared in DodoSSH.Client.Session, and it lives in
|
|
Platform/ beside the Windows and macOS stores. The Android head has its own keystore and must
|
|
never see this one.
|
|
-->
|
|
<PackageReference Include="Microsoft.TSS" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<!--
|
|
The shell's view models, the terminal renderer's files and the palette, all shared with the Android
|
|
head. This project is now the desktop *views* and the desktop platform integration, and nothing else.
|
|
-->
|
|
<ProjectReference Include="../DodoSSH.Client.Shell/DodoSSH.Client.Shell.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Import/DodoSSH.Client.Import.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Session/DodoSSH.Client.Session.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Ssh/DodoSSH.Client.Ssh.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.ObjectStore/DodoSSH.Client.ObjectStore.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Terminal/DodoSSH.Client.Terminal.csproj" />
|
|
<ProjectReference Include="../DodoSSH.Client.Transfer/DodoSSH.Client.Transfer.csproj" />
|
|
</ItemGroup>
|
|
|
|
<ItemGroup>
|
|
<!--
|
|
The view models are plain CommunityToolkit.Mvvm objects and need no Avalonia to run, so the shell's
|
|
state machine is testable as ordinary code. That is the whole reason the sign-in step is a delegate.
|
|
-->
|
|
<InternalsVisibleTo Include="DodoSSH.Client.App.Tests" />
|
|
|
|
<!--
|
|
The layout harness needs the views and the application class themselves, not just the view models:
|
|
its whole job is to lay out the real XAML and measure it.
|
|
-->
|
|
<InternalsVisibleTo Include="DodoSSH.Client.App.Layout.Tests" />
|
|
</ItemGroup>
|
|
|
|
</Project>
|
|
|