Skip to content

[FastDev] Add faster FastDeploy2#11698

Draft
simonrozsival wants to merge 17 commits into
dotnet:mainfrom
simonrozsival:android-fastdeploy2-clean
Draft

[FastDev] Add faster FastDeploy2#11698
simonrozsival wants to merge 17 commits into
dotnet:mainfrom
simonrozsival:android-fastdeploy2-clean

Conversation

@simonrozsival

@simonrozsival simonrozsival commented Jun 18, 2026

Copy link
Copy Markdown
Member

Summary

This is the cleaned-up follow-up to the experimentation PR:

This PR keeps legacy FastDeploy in place, adds a new FastDeploy2 strategy, and makes FastDeploy2 the default for app install fast deployment.

$(_AndroidFastDevStrategy)=FastDeploy|FastDeploy2

FastDeploy remains selectable as a fallback.

Review follow-up

Jon's review feedback is addressed in the latest cleanup commit:

  • FastDeploy2 no longer accepts legacy FastDeploy task inputs it does not use.
  • FastDeploy2 is now a single concrete task with no FastDeploy2Base inheritance split.
  • Development-only FastDeploy2 diagnostic/telemetry JSON was removed, so production builds only keep the functional manifest state.
  • FastDeploy2 manifest JSON serialization uses a System.Text.Json source-generated context.
  • Repeated remote path, shell quoting, and directory grouping helpers are consolidated.
  • Remote staging now uses Android's writable /data/local/tmp/fastdeploy2 instead of /tmp/fastdeploy2, because /tmp is read-only on CI emulators.
  • The local manifest now records device/package/user/ABI identity, and device-side staging/override state is validated with manifest hash markers read in a single adb shell call.
  • FastDeploy2 diagnostic logging now records exact adb commands, exit codes, stdout, and stderr for device-state sanity checks.

A fresh dotnet new maui Android sample build binlog is available here:

CI on-device test fixes

The first full CI run surfaced regressions in existing fast-deployment device tests now that FastDeploy2 is the default. Three deterministic root causes were found (each reproduced and verified on an API 36 emulator) and fixed:

  • Missing sync log markers. IncrementalFastDeployment and SkipFastDevAlreadyInstalledFile assert per-file NotifySync CopyFile / NotifySync SkipCopyFile markers. FastDeploy2 only logged Prepared X => Y, so these tests failed even though deployment succeeded. FastDeploy2 now emits the markers from the manifest's changed-file set, and the abi-skip marker uses the same device-relative identifier for consistency.
  • Symlink mode followed subdirectory symlinks into staging. The full rewrite used ln -sf "$s"/* ., which also symlinked staging subdirectories. Processing a child directory then cd'd through that symlink into the shell-owned /data/local/tmp staging area, and rm -rf ./* failed with "Permission denied" under run-as (XA0133 / XA0129). This broke any app with culture/satellite subdirectories (LocalizedAssemblies_ShouldBeFastDeployed, DotNetNewAndroidTest). The full rewrite now clears and symlinks only the regular files in each directory and leaves subdirectories to their own iterations, so it no longer follows or deletes child directories.
  • Copy-fallback stat format split by the device shell. find ... -exec stat -c "%n|%s|%Y" {} + reached the device shell unquoted, so | was treated as a pipe (exit 127 → XA0129) and the copy fallback also failed. The command is now built as a single string with the format single-quoted (matching the RunAsShell quoting pattern).
  • Device-state probe printf '\n' arrived as a literal \n. The readiness check built its probe with printf '\noverride=' / printf '\n'; the backslash escape does not survive the adb + device-shell quoting layers, so the device emitted remote=<hash>\noverride=<hash>\n on a single line. ParseDeviceManifestState (splitting on real newlines) put the whole tail into RemoteHash and left OverrideHash empty, so remoteReady was always false and every incremental install reset the staging directory and redeployed all files (changed files: 329 in CI), reporting unchanged assemblies as CopyFile. The probe is now built with echo + command substitution (real newlines, no backslash escapes); verified on an API 36 emulator that remoteReady becomes true on an unchanged reinstall.

FastDeploy2 approach

FastDeploy2 uses the final direction from the experiment PR:

  1. Build a local manifest of the deployment target and FastDev inputs:

    device id, package, Android user, ABI
    target path -> local source path, size, mtime
    
  2. Compare with the last successful deploy manifest, then verify that the device-side staging tree and app override tree both carry the expected manifest hash.

  3. Push only changed files to device temp storage, without adb push --sync:

    adb push -z any changed-file-1 changed-file-2 /data/local/tmp/fastdeploy2/<package>/<user>/<abi>/
  4. Remove stale staged files for removed inputs.

  5. Maintain files/.__override__ as symlinks to staged files with batched run-as sh -c commands. The generated shell scripts use short d/s variables plus cd so long source/target directories are not repeated for every file.

  6. After a successful deploy, write the manifest and matching device-side manifest hash markers:

    /data/local/tmp/fastdeploy2/<package>/<user>/.fastdeploy2-manifest-hash
    files/.__override__/.fastdeploy2-manifest-hash
    

Why this shape is viable now

The existing FastDeploy v2.0 design was added when Android 11 broke the older external-storage based fast deployment model. At that time, adb compression and multi-file push/sync behavior were still new: Android SDK Platform-Tools 30.0.0 (April 2020) introduced client-side compression for adb {push,pull,sync} when used with Android 11 devices, and Platform-Tools 30.0.5 (November 2020) later fixed adb push --sync with multiple inputs and improved pushing many files over high-latency links.

Today it is more reasonable to depend on a modern Android SDK Platform-Tools package for developer inner-loop scenarios. adb is supplied by the Android SDK Platform-Tools package, and developers using .NET for Android / MAUI can update it independently of end-user device support. With Platform-Tools 36.0.0, adb push -z any was also tested against API 24 and API 29 emulators that did not advertise sendrecv_v2* compression features; both accepted the command and transferred the file, apparently negotiating down to uncompressed transfer.

This PR also no longer relies on adb push --sync: the local manifest tells us exactly which files changed, so FastDeploy2 pushes only those files and avoids an adb-side scan of the full directory.

What else we tried but abandoned

From the experiment PR:

  • Existing FastDeploy is excellent for very small changes but very slow for full payloads.
  • adb push --sync over the whole directory is much faster for full payloads, but still scans/probes too much for tiny changes.
  • One adb push per file is too slow because every adb invocation has ~40ms median overhead even when skipped.
  • A local manifest gives us the changed set without probing all files through adb.
  • Symlinks avoid app-private copy after the initial setup.
  • A shell-based symlink update was faster than a native helper (maui.link), so this PR does not include a native helper.

How legacy FastDeploy could be improved instead

If we wanted to keep the native FastDeploy tool model, the most promising improvement would be to make xamarin.sync batch-aware. Instead of invoking run-as ... xamarin.sync once per changed file, the host could open a single run-as <package> xamarin.sync-batch stdin stream and write all changed files one after another using a small binary protocol:

magic/version
file-count
repeat file-count times:
  target-path-length
  target-path
  uncompressed-size
  mtime
  compressed-blocks
removed-file-count
repeat removed-file-count times:
  target-path-length
  target-path

The device helper would create directories, write each file to a temporary path, set mtime/permissions, rename into place, and remove stale files. This would keep the existing app-private real-file model and avoid symlink assumptions, while removing most per-file adb/run-as process overhead.

That approach is intentionally not part of this PR because it keeps the custom native tool/protocol complexity. FastDeploy2 tries the simpler path first: use adb's built-in transfer/compression support, keep the changed-file decision local, and leave legacy FastDeploy available as a fallback while we validate the symlink approach across more devices.

Preliminary benchmark from the experiment PR

Device: physical Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V) connected by USB 3 cable
Project: samples/HelloWorld/HelloWorld/HelloWorld.DotNet.csproj

Strategy Case Deploy task Wall Upload/transfer Changed
FastDeploy (existing) first 22.11s 31.61s 19.09s 194
FastDeploy (existing) one DLL 0.68s 10.10s 0.14s 2
FastDeploy (existing) app DLLs 0.69s 10.10s 0.20s 3
FastDeploy (existing) all DLLs 17.72s 27.27s 17.16s 189
FastDeploy2 first 5.41s 14.51s 3.09s 195
FastDeploy2 one DLL 0.56s 9.70s 0.06s 2
FastDeploy2 app DLLs 0.59s 9.99s 0.04s 3
FastDeploy2 all DLLs 3.02s 12.40s 2.42s 189

Validation

Focused task builds pass in this branch:

dotnet build-server shutdown
 dotnet build build-tools/xa-prep-tasks/xa-prep-tasks.csproj --nologo -v:minimal -m:1
 dotnet build src/Xamarin.Android.Build.Debugging.Tasks/Xamarin.Android.Build.Debugging.Tasks.csproj --no-restore --nologo -v:minimal -m:1
 dotnet build build-tools/xa-prep-tasks/xa-prep-tasks.csproj -c Release --nologo -v:minimal -m:1
 dotnet build src/Xamarin.Android.Build.Debugging.Tasks/Xamarin.Android.Build.Debugging.Tasks.csproj -c Release --no-restore --nologo -v:minimal -m:1

Device/emulator validation passed:

Device Scenario Result
Physical Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V) over USB 3 cable clean first install with default FastDeploy2 symlink mode passed; app launched successfully
Physical Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V) over USB 3 cable one-DLL incremental install with default FastDeploy2 symlink mode passed; app launched successfully
Physical Samsung Galaxy A16 (SM-A165F, R58Y30HZ65V) over USB 3 cable one-DLL incremental install with copy fallback mode passed; app launched successfully
API 36 emulator clean first install with shortened d/s shell-variable symlink scripts passed; symlink tree created successfully
API 36 emulator Symlink-mode install followed by Copy-mode install passed; symlinks were replaced by regular files and the symlink marker was removed

Observed validation details:

first default: deploy=15.84s, mode=Symlink, changed=195, pushed=195, symlink update=161ms
one DLL default: deploy=0.89s, mode=Symlink, changed=2, pushed=2, symlink update=4ms
one DLL Copy fallback: deploy=2.80s, mode=Copy, changed=2, pushed=2, copy=977ms

Additional adb compatibility probes with Platform-Tools 36.0.0:

Target Device adb features adb push -z any
API 24 emulator cmd only, no sendrecv_v2* passed
API 29 emulator cmd only, no sendrecv_v2* passed
API 36 emulator sendrecv_v2, brotli, lz4, zstd passed

Risks

  • Legacy FastDeploy is still present and selectable.
  • Symlinks from app-private files/.__override__ to /data/local/tmp/fastdeploy2/... worked on the tested physical Samsung device over USB.
  • The new approach has only been tested on a single device from a single vendor so far; it needs broader Android/OEM/API/device testing.
  • If symlink creation fails, this code can fallback to copy mode and clears symlink-managed override state before copying.
  • If the local manifest is missing, does not match the current device/package/user/ABI, or does not match the device-side manifest hash marker, FastDeploy2 does a full push.
  • Developers with very old Android SDK Platform-Tools may need to update adb for adb push -z any; older devices can still fall back to uncompressed transfer when driven by a modern adb.

Future work

  • Delete the old FastDeploy implementation once we are confident the new approach is better across a broader device matrix.
  • Delete the old native FastDeploy helper tools once FastDeploy no longer needs them.
  • If symlink validation across devices is not good enough, consider a batched xamarin.sync protocol as the app-private real-file fallback design.

Add a new FastDeploy2 strategy that uses a local manifest to push only changed files to temporary device storage and mirrors the app override directory with shell-created symlinks. Keep legacy FastDeploy selectable while making FastDeploy2 the default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove existing override paths before copying changed files so Copy mode can safely recover from a symlink-based override tree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival marked this pull request as ready for review June 18, 2026 10:30
Copilot AI review requested due to automatic review settings June 18, 2026 10:30

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a new manifest-driven fast deployment implementation (FastDeploy2) alongside the legacy FastDeploy, and makes FastDeploy2 the default strategy for app install fast deployment.

Changes:

  • Register and invoke the new FastDeploy2 MSBuild task, with _AndroidFastDevStrategy defaulting to FastDeploy2.
  • Add FastDeploy2Base (core orchestration + adb/run-as helpers) plus a manifest-based FastDeploy2 implementation that pushes only changed files and updates overrides via symlink (with copy fallback).
  • Add MSBuild-time validation for supported strategy/transfer-mode values, and a new adb compression algorithm property default (any).
Show a summary per file
File Description
src/Xamarin.Android.Build.Debugging.Tasks/Xamarin.Android.Common.Debugging.targets Registers FastDeploy2, selects strategy, sets defaults for strategy/transfer mode/compression, and wires task invocation into _Upload.
src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Adds FastDeploy2Base task implementation: device/package orchestration, adb invocation, file staging/stat/diff/copy paths, diagnostics logging, and error handling.
src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Implements manifest-driven incremental upload, remote “ready” marker handling, stale cleanup, and shell-symlink override updates with copy fallback.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 8

Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Outdated
@simonrozsival simonrozsival changed the title [WIP] Add manifest-driven FastDeploy2 [FastDev] Add manifest-driven FastDeploy2 Jun 18, 2026
simonrozsival and others added 4 commits June 18, 2026 13:01
Fix manifest reset handling, device-scoped manifest state, adb batching edge cases, symlink/copy recovery, and diagnostics logging concurrency.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use shell variables and cd to avoid repeating long staging and override paths in generated run-as symlink scripts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Drop leftover experimental staging and symlink helper methods that are no longer referenced by the manifest-driven direct-push implementation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Skip missing FastDev inputs before manifest creation and clear symlink-managed override state before Copy mode so stale symlinks cannot survive mode switches or fallback.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival changed the title [FastDev] Add manifest-driven FastDeploy2 [FastDev] Add faster FastDeploy2 Jun 18, 2026
@simonrozsival

Copy link
Copy Markdown
Member Author

/review

@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown

Android PR Reviewer completed successfully!

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Needs Changes

Thorough review of the new FastDeploy2 implementation. The architecture is well thought out — manifest-based incremental diffing, a clean strategy/mode split in the targets with condition-first <Error> validation, careful shell-argument quoting (ShellQuote single-quote escaping) and package-name sanitization (IsSafePackageNameForShell) to prevent shell injection, ADB cancellation handling with process kill + output draining, and reuse of the existing XA012x/XA013x error codes. Nice work.

A few things to address before merge:

Issues by severity

  • ⚠️ 2 warnings
    • Order-dependent shell globbing (FastDeploy2.Manifest.cs:132): the full-refresh rm -f ./* / ln -sf "$s"/* . globs also match subdirectories such as the staged {abi}/environment. Because directories is iterated in nondeterministic HashSet order, this can either delete real staged files through a directory symlink or silently fall back to copy. Please handle nested directories explicitly.
    • Dead/duplicated base implementation (FastDeploy2.cs:549): the virtual DeployFastDevFilesWithAdbPush and its exclusive helpers are fully superseded by the override and never run — including a duplicated CreatePushArgsPrefix that still uses --sync. Recommend deleting it (or making the method abstract).
  • 💡 2 suggestions
    • MarkRemoteReady ignores the adb exit code (FastDeploy2.Manifest.cs:447).
    • ManifestEntry.LocalPath is persisted but never read (FastDeploy2.Manifest.cs:496).

See the inline comments for mechanisms and concrete fixes.

CI: license/cla passed; the Azure DevOps builds (Linux/Mac/Windows) are still in progress at review time, so green CI is not yet confirmed — please ensure the internal pipeline passes before merge.

The symlink-glob behavior is the main correctness concern; the rest are maintainability cleanups. I couldn't reproduce on-device, so if I've misread the staging layout for the glob case, a quick confirmation of how {abi}/environment is handled during a full refresh would resolve it.

Generated by Android PR Reviewer for issue #11698 · 1.4K AIC · ⌖ 48.9 AIC · ⊞ 37.8K
Comment /review to run again

Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Outdated
Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Outdated

@jonathanpeppers jonathanpeppers left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I want to see a .binlog of a deploy to understand what it does -- I might have time to manually test some later.

Overall, I like what it does -- introduce a private property by default you can opt out of if something goes wrong.

Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.cs Outdated
Comment on lines +478 to +489
static string GetSafeFileName (string value)
{
if (string.IsNullOrEmpty (value)) {
return "_";
}

var builder = new StringBuilder (value.Length);
foreach (char c in value) {
builder.Append (char.IsLetterOrDigit (c) || c == '.' || c == '-' || c == '_' ? c : '_');
}
return builder.ToString ();
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an existing helper somewhere that does this? I can think of maybe things related to Java names, or Android resources.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I looked and couldn't find an existing helper for turning an arbitrary string into a filesystem-safe path segment. The Java-name and Android-resource helpers (identifier mangling / resource-designer naming) solve a different problem with different rules — they're about valid C#/Java identifiers, not reversible, collision-free directory names. Here we just need a unique cache directory per device/package/user/abi, which is what Uri.EscapeDataString gives (reversible, no path separators). Happy to switch to a shared helper if you have one in mind — leaving this thread open for your call.

Comment thread src/Xamarin.Android.Build.Debugging.Tasks/Tasks/FastDeploy2.Manifest.cs Outdated
@simonrozsival

Copy link
Copy Markdown
Member Author

@jonathanpeppers I captured a binlog from building a fresh dotnet new maui sample app with this branch's local SDK:

https://gist.github.com/simonrozsival/c06cacff00c99ee3504c256205e24492

Command used for the successful binlog:

./dotnet-local.sh build JonMauiBinlog/JonMauiBinlog.csproj \
  -f net11.0-android \
  -p:TargetFrameworks=net11.0-android \
  -p:SupportedOSPlatformVersion=24.0 \
  -bl:JonMauiBinlog-net11.0-android.binlog

The TFM override keeps the template Android-only because I only installed maui-android locally; the supported OS override matches the local Android SDK minimum of 24.

simonrozsival and others added 2 commits June 18, 2026 16:22
Move FastDeploy2 diagnostic JSON helpers out of the main task, use System.Text.Json source generation for FastDeploy2 JSON, remove unused FastDeploy2 task inputs, and consolidate repeated path/grouping helpers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Flatten FastDeploy2 into a single concrete task, remove the development diagnostics property bag and JSON payload, and keep only the functional manifest serialization state.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers

Copy link
Copy Markdown
Member

@jonathanpeppers I captured a binlog from building a fresh dotnet new maui sample app with this branch's local SDK:

https://gist.github.com/simonrozsival/c06cacff00c99ee3504c256205e24492

Command used for the successful binlog:

./dotnet-local.sh build JonMauiBinlog/JonMauiBinlog.csproj \
  -f net11.0-android \
  -p:TargetFrameworks=net11.0-android \
  -p:SupportedOSPlatformVersion=24.0 \
  -bl:JonMauiBinlog-net11.0-android.binlog

The TFM override keeps the template Android-only because I only installed maui-android locally; the supported OS override matches the local Android SDK minimum of 24.

I think your command needs -t:Install to see the FastDeploy2 task run.

@simonrozsival

Copy link
Copy Markdown
Member Author

@jonathanpeppers you're right — the earlier binlog was build-only. I updated the same gist with a new -t:Install binlog that includes the FastDeploy2 task:

https://gist.github.com/simonrozsival/c06cacff00c99ee3504c256205e24492

New file:

JonMauiBinlog-net11.0-android-install-fastdeploy2.binlog

Command used:

./dotnet-local.sh build JonMauiBinlog/JonMauiBinlog.csproj \
  -t:Install \
  -f net11.0-android \
  --no-restore \
  -m:1 \
  -nr:false \
  -p:TargetFrameworks=net11.0-android \
  -p:SupportedOSPlatformVersion=24.0 \
  -p:AdbTarget=-s%20R58Y30HZ65V \
  -p:_FastDeploymentDiagnosticLogging=False \
  -bl:JonMauiBinlog-net11.0-android-install-fastdeploy2.binlog \
  -clp:PerformanceSummary \
  -v:minimal

The console log shows FastDeploy2 ran and the build succeeded:

11089 ms  FastDeploy2  1 calls
Build succeeded.

Avoid logging empty run-as command output and buffer optional missing-file messages behind FastDeploy2 diagnostics so normal install binlogs stay readable.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@jonathanpeppers

Copy link
Copy Markdown
Member

There are many log messages that just say rm returned:

image

I think we should probably skip these messages in general unless _FastDeploymentDiagnosticLogging=true. And maybe it should not log if blank?

But then I probably need to see a log where _FastDeploymentDiagnosticLogging is true.

Stage FastDeploy2 files under /data/local/tmp instead of /tmp so Android emulators with read-only /tmp can install. Also remove existing override contents recursively before full symlink refreshes so resource/culture directories do not fail rm.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival

Copy link
Copy Markdown
Member Author

@jonathanpeppers to clarify the physical-device validation: that was a Samsung Galaxy A16 (SM-A165F, serial R58Y30HZ65V) connected over a USB 3 cable, not an emulator and not Wi-Fi adb.

The emulator validation rows in the PR description are separate API 24/29/36 emulator checks.

Add target identity to FastDeploy2 manifests and store the manifest hash on both the remote staging tree and the app override tree. Read both device-side hashes in one adb shell command before deciding whether incremental deploy state can be trusted, and expand adb diagnostics to include exact commands, exit codes, stdout, and stderr.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@simonrozsival simonrozsival marked this pull request as draft June 22, 2026 11:23
simonrozsival and others added 5 commits June 28, 2026 00:37
FastDeploy2 (the new default fast-deployment strategy) broke several
existing on-device tests. Three deterministic, device-verified root causes:

- Missing sync log markers: tests assert `NotifySync CopyFile`/
  `NotifySync SkipCopyFile` per file. FastDeploy2 only logged
  `Prepared X => Y`, so IncrementalFastDeployment and
  SkipFastDevAlreadyInstalledFile failed even though deployment
  succeeded. Emit the markers from the manifest's changed-file set.

- Symlink mode followed subdirectory symlinks into staging: the full
  rewrite used `ln -sf "$s"/* .`, which also symlinked staging
  subdirectories. Processing a child directory then `cd`'d through that
  symlink into the shell-owned /data/local/tmp staging area and
  `rm -rf ./*` failed with "Permission denied" under run-as (XA0133 /
  XA0129). This broke any app with culture/satellite subdirectories
  (LocalizedAssemblies_ShouldBeFastDeployed, DotNetNewAndroidTest). Only
  symlink regular files, and process parent directories before children
  so a parent's `rm -rf ./*` cannot delete a freshly created child.

- Copy fallback's stat format was split by the device shell: the
  `find ... -exec stat -c "%n|%s|%Y" {} +` argv reached the device shell
  unquoted, so `|` was treated as a pipe (exit 127, XA0129). Build the
  command as a single string with the format single-quoted, matching the
  RunAsShell quoting pattern.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Self-review follow-up. The previous fix replaced `ln -sf "$s"/* .` with a
files-only loop but still ran `rm -rf ./*`, which deletes child directories.
The old glob accidentally masked an incremental-deploy bug by symlinking whole
staging subdirectories: on an incremental deploy where a parent directory gains
all-new files while a child directory keeps unchanged files, the parent's
`rm -rf ./*` wiped the child's still-valid symlinks.

Clear only the regular files in each directory (`for e in ./*; do
[ -d "$e" ] || rm -f "$e"; done`) so subdirectories and their contents survive,
and drop the now-unnecessary parents-first ordering. Verified on an emulator:
the parent relinks its files, the child subdirectory and its symlink are
preserved, and the staging area is untouched.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Self-review follow-up. The abi-skip `NotifySync SkipCopyFile` marker logged
the local build path (file.ItemSpec) while the per-file sync markers log the
device-relative target path, making the NotifySync marker family inconsistent
within the task. Use GetAdbPushTargetPath for the abi-skip marker too so all
markers report the same device-relative identifier.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The device-state readiness check built its probe with
`printf '\noverride='` / `printf '\n'`. The `\n` escape does not survive
the adb + device-shell quoting layers and arrives as a literal two-character
"\n", so the device emits a single line:

  remote=<hash>\noverride=<hash>\n

ParseDeviceManifestState splits on real newlines, so the whole tail lands in
RemoteHash and OverrideHash stays empty. RemoteHash never equals the previous
manifest hash, `remoteReady` is always false, and every incremental install
resets the staging directory and redeploys *all* files (observed as
`changed files: 329` in CI), so unchanged assemblies are reported as
NotifySync CopyFile instead of SkipCopyFile.

Build the probe with `echo` + command substitution instead, which emits real
newlines and contains no backslash escapes to be mangled. Verified on an API 36
emulator: the device now returns two parseable lines and `remoteReady` becomes
true on an unchanged reinstall.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
ManifestEntry.LocalPath was written into manifest.json and folded into the
device-side manifest hash, but it is host-specific (an absolute build path) and
nothing reads it for change detection (GetChangedFiles compares Size and
LastWriteTimeUtcTicks; the dictionary key is RelativePath). Persisting and
hashing it bloats the manifest and couples the device readiness check to the
build machine's paths for no benefit. Remove the field, its JSON property, and
its contribution to the canonical manifest text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants