Skip to content

Commit b7a0793

Browse files
committed
Enable self-contained NativeAOT deployment
NativeAOT compiles managed projects to native binaries, eliminating the .NET runtime dependency. This fixes CI functional tests which run on machines without .NET 10 installed. Changes: - Add SelfContained=true and PublishAot=true to Directory.Build.props - Add OptimizationPreference=Speed for performance-critical system component - Opt out test projects from AOT (NUnit uses reflection for discovery) - Opt out GVFS.MSBuild (netstandard2.0 build task) - Switch Build.bat from 'dotnet build' to 'dotnet publish' for managed projects - Update all script output paths to include publish\ subdirectory - Pin global.json to SDK 10.0.202 with rollForward=disable Output: ~20 MB native GVFS.exe, 36.7 MB installer (vs 107 MB self-contained) Assisted-by: Claude Opus 4.6 Signed-off-by: Tyrie Vella <tyrielv@gmail.com>
1 parent 7835f0c commit b7a0793

21 files changed

Lines changed: 177 additions & 49 deletions

.github/workflows/functional-tests.yaml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,18 @@ jobs:
122122
shell: cmd
123123
run: gvfs\install.bat
124124

125+
- name: Verify GVFS installation
126+
if: steps.skip.outputs.result != 'true'
127+
shell: cmd
128+
continue-on-error: true
129+
run: |
130+
echo === GVFS Version ===
131+
"C:\Program Files\VFS for Git\GVFS.exe" version
132+
echo === Service Status ===
133+
sc query GVFS.Service
134+
echo === List Mounted ===
135+
"C:\Program Files\VFS for Git\GVFS.exe" service --list-mounted
136+
125137
- name: ProjFS details (post-install)
126138
if: steps.skip.outputs.result != 'true'
127139
shell: cmd
@@ -138,9 +150,39 @@ jobs:
138150
git\logs
139151
gvfs\logs
140152
153+
- name: Test gvfs clone
154+
if: steps.skip.outputs.result != 'true'
155+
shell: cmd
156+
timeout-minutes: 5
157+
run: |
158+
SET PATH=C:\Program Files\VFS for Git;%PATH%
159+
mkdir C:\temp 2>nul
160+
echo === Testing gvfs clone (system service) ===
161+
gvfs clone https://gvfs.visualstudio.com/ci/_git/ForTests C:\gvfs-smoke-test --no-prefetch 2>&1
162+
echo === Clone exit code: %ERRORLEVEL% ===
163+
echo === gvfs status ===
164+
gvfs status C:\gvfs-smoke-test 2>&1
165+
echo === Status exit code: %ERRORLEVEL% ===
166+
echo === Install test service ===
167+
sc create Test.GVFS.Service binPath= "\"C:\Program Files\VFS for Git\GVFS.Service.exe\"" 2>&1
168+
sc start Test.GVFS.Service --servicename=Test.GVFS.Service 2>&1
169+
timeout /t 3 /nobreak >nul
170+
sc query Test.GVFS.Service 2>&1
171+
echo === Testing gvfs clone (test service) ===
172+
gvfs clone https://gvfs.visualstudio.com/ci/_git/ForTests C:\gvfs-smoke-test2 --no-prefetch --internal "{\"ServiceName\":\"Test.GVFS.Service\",\"StartedByService\":false,\"MaintenanceJob\":null,\"PackfileMaintenanceBatchSize\":null}" 2>&1
173+
echo === Test service clone exit code: %ERRORLEVEL% ===
174+
echo === Cleanup ===
175+
gvfs unmount C:\gvfs-smoke-test 2>&1
176+
gvfs unmount C:\gvfs-smoke-test2 2>&1
177+
sc stop Test.GVFS.Service 2>&1
178+
sc delete Test.GVFS.Service 2>&1
179+
rmdir /s /q C:\gvfs-smoke-test 2>nul
180+
rmdir /s /q C:\gvfs-smoke-test2 2>nul
181+
141182
- name: Run functional tests
142183
if: steps.skip.outputs.result != 'true'
143184
shell: cmd
185+
timeout-minutes: 60
144186
run: |
145187
SET PATH=C:\Program Files\VFS for Git;%PATH%
146188
SET GIT_TRACE2_PERF=C:\temp\git-trace2.log

Directory.Build.props

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@
2626
<LangVersion>latest</LangVersion>
2727
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
2828
<PlatformTarget>x64</PlatformTarget>
29+
<SelfContained>true</SelfContained>
30+
<PublishAot>true</PublishAot>
31+
<OptimizationPreference>Speed</OptimizationPreference>
2932
<BaseOutputPath>$(ProjectOutPath)bin\</BaseOutputPath>
3033
<BaseIntermediateOutputPath>$(ProjectOutPath)obj\</BaseIntermediateOutputPath>
3134
</PropertyGroup>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using GVFS.Common.Http;
2+
using GVFS.Common.NamedPipes;
3+
using GVFS.Common.Tracing;
4+
using GVFS.Common.Prefetch;
5+
using System;
6+
using System.Collections.Generic;
7+
using System.Text.Json;
8+
using System.Text.Json.Serialization;
9+
10+
namespace GVFS.Common
11+
{
12+
/// <summary>
13+
/// Source-generated JSON serializer context for all types used in GVFS serialization.
14+
/// This enables trim-safe and AOT-compatible JSON serialization without reflection.
15+
/// </summary>
16+
[JsonSourceGenerationOptions(
17+
PropertyNameCaseInsensitive = true,
18+
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
19+
Converters = new[] { typeof(VersionConverter) })]
20+
[JsonSerializable(typeof(string))]
21+
[JsonSerializable(typeof(Dictionary<string, string>))]
22+
[JsonSerializable(typeof(KeyValuePair<string, string>))]
23+
[JsonSerializable(typeof(List<string>))]
24+
[JsonSerializable(typeof(List<GitObjectsHttpRequestor.GitObjectSize>))]
25+
[JsonSerializable(typeof(ServerGVFSConfig))]
26+
[JsonSerializable(typeof(VersionResponse))]
27+
[JsonSerializable(typeof(InternalVerbParameters))]
28+
[JsonSerializable(typeof(CacheServerInfo))]
29+
[JsonSerializable(typeof(NamedPipeMessages.GetStatus.Response), TypeInfoPropertyName = "GetStatusResponse")]
30+
[JsonSerializable(typeof(NamedPipeMessages.DehydrateFolders.Request), TypeInfoPropertyName = "DehydrateFoldersRequest")]
31+
[JsonSerializable(typeof(NamedPipeMessages.DehydrateFolders.Response), TypeInfoPropertyName = "DehydrateFoldersResponse")]
32+
[JsonSerializable(typeof(NamedPipeMessages.Notification.Request), TypeInfoPropertyName = "NotificationRequest")]
33+
[JsonSerializable(typeof(NamedPipeMessages.UnregisterRepoRequest))]
34+
[JsonSerializable(typeof(NamedPipeMessages.UnregisterRepoRequest.Response), TypeInfoPropertyName = "UnregisterRepoResponse")]
35+
[JsonSerializable(typeof(NamedPipeMessages.RegisterRepoRequest))]
36+
[JsonSerializable(typeof(NamedPipeMessages.RegisterRepoRequest.Response), TypeInfoPropertyName = "RegisterRepoResponse")]
37+
[JsonSerializable(typeof(NamedPipeMessages.EnableAndAttachProjFSRequest))]
38+
[JsonSerializable(typeof(NamedPipeMessages.EnableAndAttachProjFSRequest.Response), TypeInfoPropertyName = "EnableAndAttachProjFSResponse")]
39+
[JsonSerializable(typeof(NamedPipeMessages.GetActiveRepoListRequest))]
40+
[JsonSerializable(typeof(NamedPipeMessages.GetActiveRepoListRequest.Response), TypeInfoPropertyName = "GetActiveRepoListResponse")]
41+
[JsonSerializable(typeof(NamedPipeMessages.BaseResponse<string>))]
42+
[JsonSerializable(typeof(TelemetryDaemonEventListener.PipeMessage))]
43+
[JsonSerializable(typeof(PrettyConsoleEventListener.ConsoleOutputPayload))]
44+
internal partial class GVFSJsonContext : JsonSerializerContext
45+
{
46+
}
47+
}

GVFS/GVFS.Common/GVFSJsonOptions.cs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,47 @@
1+
using GVFS.Common.Tracing;
12
using System;
3+
using System.Diagnostics.CodeAnalysis;
24
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
using System.Text.Json.Serialization.Metadata;
37

48
namespace GVFS.Common
59
{
6-
/// <summary>
7-
/// Shared JsonSerializerOptions and helpers for the GVFS codebase.
8-
/// PropertyNameCaseInsensitive preserves backward compatibility with
9-
/// Newtonsoft.Json's default case-insensitive deserialization.
10-
/// </summary>
1110
public static class GVFSJsonOptions
1211
{
12+
[UnconditionalSuppressMessage("AOT", "IL2026",
13+
Justification = "DefaultJsonTypeInfoResolver fallback handles all types at runtime.")]
14+
[UnconditionalSuppressMessage("AOT", "IL3050",
15+
Justification = "DefaultJsonTypeInfoResolver fallback handles all types at runtime.")]
1316
public static readonly JsonSerializerOptions Default = new JsonSerializerOptions
1417
{
1518
PropertyNameCaseInsensitive = true,
16-
Converters = { new VersionConverter(), new Tracing.EventMetadataConverter() },
19+
Converters = { new VersionConverter(), new EventMetadataConverter() },
20+
TypeInfoResolverChain = { GVFSJsonContext.Default, new DefaultJsonTypeInfoResolver() },
1721
};
1822

19-
/// <summary>
20-
/// Serialize using the compile-time type. Use when <typeparamref name="T"/>
21-
/// is the concrete type (not a base class with derived properties).
22-
/// </summary>
23+
[UnconditionalSuppressMessage("AOT", "IL2026",
24+
Justification = "TypeInfoResolverChain includes DefaultJsonTypeInfoResolver.")]
25+
[UnconditionalSuppressMessage("AOT", "IL3050",
26+
Justification = "TypeInfoResolverChain includes DefaultJsonTypeInfoResolver.")]
2327
public static string Serialize<T>(T value)
2428
{
2529
return JsonSerializer.Serialize(value, Default);
2630
}
2731

28-
/// <summary>
29-
/// Serialize using the runtime type. Use when calling from a base-class
30-
/// method where compile-time type would lose derived-class properties
31-
/// (e.g., BaseResponse&lt;T&gt;.ToMessage()).
32-
/// </summary>
32+
[UnconditionalSuppressMessage("AOT", "IL2026",
33+
Justification = "TypeInfoResolverChain includes DefaultJsonTypeInfoResolver.")]
34+
[UnconditionalSuppressMessage("AOT", "IL3050",
35+
Justification = "TypeInfoResolverChain includes DefaultJsonTypeInfoResolver.")]
3336
public static string Serialize(object value, Type inputType)
3437
{
3538
return JsonSerializer.Serialize(value, inputType, Default);
3639
}
3740

41+
[UnconditionalSuppressMessage("AOT", "IL2026",
42+
Justification = "TypeInfoResolverChain includes DefaultJsonTypeInfoResolver.")]
43+
[UnconditionalSuppressMessage("AOT", "IL3050",
44+
Justification = "TypeInfoResolverChain includes DefaultJsonTypeInfoResolver.")]
3845
public static T Deserialize<T>(string json)
3946
{
4047
return JsonSerializer.Deserialize<T>(json, Default);

GVFS/GVFS.Common/Tracing/PrettyConsoleEventListener.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ protected override void RecordMessageInternal(TraceEventMessage message)
5959
}
6060
}
6161

62-
private class ConsoleOutputPayload
62+
internal class ConsoleOutputPayload
6363
{
6464
public string ErrorMessage { get; set; }
6565
}

GVFS/GVFS.FunctionalTests.LockHolder/GVFS.FunctionalTests.LockHolder.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5+
<PublishAot>false</PublishAot>
56
</PropertyGroup>
67

78
<ItemGroup>

GVFS/GVFS.FunctionalTests/GVFS.FunctionalTests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5+
<PublishAot>false</PublishAot>
56
</PropertyGroup>
67

78
<ItemGroup>

GVFS/GVFS.FunctionalTests/Program.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,11 @@ public static void Main(string[] args)
130130
?? Properties.Settings.Default.RepoToClone;
131131

132132
RunBeforeAnyTests();
133+
Console.WriteLine("[DEBUG] Starting test execution...");
134+
Console.Out.Flush();
133135
Environment.ExitCode = runner.RunTests(includeCategories, excludeCategories, testSlice);
136+
Console.WriteLine("[DEBUG] Test execution complete. ExitCode=" + Environment.ExitCode);
137+
Console.Out.Flush();
134138

135139
if (Debugger.IsAttached)
136140
{
@@ -141,12 +145,21 @@ public static void Main(string[] args)
141145

142146
private static void RunBeforeAnyTests()
143147
{
148+
Console.WriteLine("[DEBUG] RunBeforeAnyTests: starting");
149+
Console.Out.Flush();
150+
144151
if (GVFSTestConfig.ReplaceInboxProjFS)
145152
{
153+
Console.WriteLine("[DEBUG] Replacing inbox ProjFS");
154+
Console.Out.Flush();
146155
ProjFSFilterInstaller.ReplaceInboxProjFS();
147156
}
148157

158+
Console.WriteLine("[DEBUG] Installing service...");
159+
Console.Out.Flush();
149160
GVFSServiceProcess.InstallService();
161+
Console.WriteLine("[DEBUG] Service installed");
162+
Console.Out.Flush();
150163

151164
string serviceProgramDataDir = GVFSPlatform.Instance.GetSecureDataRootForGVFSComponent(
152165
GVFSConstants.Service.ServiceName);
@@ -159,6 +172,9 @@ private static void RunBeforeAnyTests()
159172
Directory.CreateDirectory(serviceProgramDataDir);
160173
File.WriteAllText(statusCacheVersionTokenPath, string.Empty);
161174
}
175+
176+
Console.WriteLine("[DEBUG] RunBeforeAnyTests: complete");
177+
Console.Out.Flush();
162178
}
163179
}
164180
}

GVFS/GVFS.FunctionalTests/Tools/GVFSServiceProcess.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,19 +160,26 @@ private static void CleanupServiceData()
160160

161161
private static void InstallWindowsService()
162162
{
163+
Console.WriteLine("[DEBUG] InstallWindowsService: starting");
164+
Console.Out.Flush();
163165
Console.WriteLine("Installing " + TestServiceName);
164166

165167
UninstallWindowsService();
168+
Console.WriteLine("[DEBUG] UninstallWindowsService: done");
169+
Console.Out.Flush();
166170

167171
// Wait for delete to complete. If the services control panel is open, this will never complete.
168172
while (RunScCommand("query", TestServiceName).ExitCode == 0)
169173
{
174+
Console.WriteLine("[DEBUG] Waiting for service delete to complete...");
175+
Console.Out.Flush();
170176
Thread.Sleep(1000);
171177
}
172178

173179
// Install service
174180
string pathToService = GetPathToService();
175181
Console.WriteLine("Using service executable: " + pathToService);
182+
Console.Out.Flush();
176183

177184
File.Exists(pathToService).ShouldBeTrue($"{pathToService} does not exist");
178185

@@ -181,10 +188,16 @@ private static void InstallWindowsService()
181188
TestServiceName,
182189
pathToService);
183190

191+
Console.WriteLine("[DEBUG] Running sc create...");
192+
Console.Out.Flush();
184193
ProcessResult result = RunScCommand("create", createServiceArguments);
185194
result.ExitCode.ShouldEqual(0, "Failure while running sc create " + createServiceArguments + "\r\n" + result.Output);
186195

196+
Console.WriteLine("[DEBUG] Starting service...");
197+
Console.Out.Flush();
187198
StartWindowsService();
199+
Console.WriteLine("[DEBUG] InstallWindowsService: complete");
200+
Console.Out.Flush();
188201
}
189202

190203
private static void UninstallWindowsService()

GVFS/GVFS.MSBuild/GVFS.MSBuild.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
<PropertyGroup>
44
<TargetFramework>netstandard2.0</TargetFramework>
5+
<SelfContained>false</SelfContained>
6+
<PublishAot>false</PublishAot>
57
<IncludeBuildOutput>false</IncludeBuildOutput>
68
</PropertyGroup>
79

0 commit comments

Comments
 (0)