Skip to content

Commit 82685db

Browse files
[gps] Fix ProcessGoogleServicesJson MSBuild target incrementality (#1304)
Fixes: #1282 The `ProcessGoogleServicesJson` target uses MSBuild's incremental build (`Inputs`/`Outputs`) which only checks file timestamps. When switching between environment-specific google-services files (e.g., `google-services-stage.json` → `google-services-test.json`), the target incorrectly skips if timestamps haven't changed. ## Changes - **Added hash-based cache tracking**: New `_ComputeGoogleServicesJsonHash` target computes SHA-256 hash of `@(GoogleServicesJson)` items and writes to `$(IntermediateOutputPath)$(MSBuildProjectFile).GoogleServicesJson.cache` - **Updated target inputs**: Changed `ProcessGoogleServicesJson` inputs from `@(GoogleServicesJson)` to `@(GoogleServicesJson);$(ProcessGoogleServicesJsonCachePath)` so MSBuild detects cache file changes when file list changes - **Clean integration**: Added cache file deletion to `_CleanProcessGoogleServicesJson` target This follows the same pattern used by the C# compiler for detecting source file list changes. When the file list or content changes, the hash differs, updating the cache file timestamp and triggering rebuild. ## Example ```xml <!-- Before: Would skip on second build --> <GoogleServicesJson Include="Platforms\Android\google-services-$(AppEnvironment).json" /> <!-- Build 1: AppEnvironment=Stage → Creates cache with stage file hash --> <!-- Build 2: AppEnvironment=Test → Cache hash differs, file updates, target reruns ✓ --> ``` Co-authored-by: Jonathan Peppers <jonathan.peppers@microsoft.com>
1 parent 5351922 commit 82685db

File tree

2 files changed

+153
-2
lines changed

2 files changed

+153
-2
lines changed

source/com.google.android.gms/play-services-basement/buildtasks.tests/BuildTaskTests.cs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,5 +93,134 @@ public void Test_Inputs_Newer_Than_Outputs()
9393
Assert.IsTrue(success);
9494
Assert.IsFalse(log.Events.Any(e => e.Message.Contains("ProcessGoogleServicesJson") && e.Message.Contains("skipped")));
9595
}
96+
97+
[Test]
98+
public void Test_Reruns_When_GoogleServicesJson_List_Changes()
99+
{
100+
// This test validates the fix for the ProcessGoogleServicesJson target being incorrectly skipped
101+
// when the GoogleServicesJson file list changes (e.g., switching between environments).
102+
// The target should rerun even if file timestamps haven't changed.
103+
104+
var googleServicesJsonPath1 = Path.Combine(TempDir, "google-services-stage.json");
105+
var googleServicesJsonPath2 = Path.Combine(TempDir, "google-services-test.json");
106+
107+
// Create two different google-services.json files with the same content structure but different values
108+
var json1 = @"{
109+
""project_info"": {
110+
""project_id"": ""stage-project""
111+
},
112+
""client"": [
113+
{
114+
""client_info"": {
115+
""android_client_info"": {
116+
""package_name"": ""com.xamarin.sample""
117+
}
118+
},
119+
""api_key"": [
120+
{
121+
""current_key"": ""STAGE_KEY""
122+
}
123+
]
124+
}
125+
]
126+
}";
127+
128+
var json2 = @"{
129+
""project_info"": {
130+
""project_id"": ""test-project""
131+
},
132+
""client"": [
133+
{
134+
""client_info"": {
135+
""android_client_info"": {
136+
""package_name"": ""com.xamarin.sample""
137+
}
138+
},
139+
""api_key"": [
140+
{
141+
""current_key"": ""TEST_KEY""
142+
}
143+
]
144+
}
145+
]
146+
}";
147+
148+
File.WriteAllText(googleServicesJsonPath1, json1);
149+
File.WriteAllText(googleServicesJsonPath2, json2);
150+
151+
var monoAndroidResDirIntermediate = Path.Combine(TempDir, "Debug");
152+
Directory.CreateDirectory(monoAndroidResDirIntermediate);
153+
154+
var engine = new ProjectCollection();
155+
var prel = ProjectRootElement.Create(Path.Combine(TempDir, "project.csproj"), engine);
156+
157+
Console.WriteLine("TempDir: {0}", TempDir);
158+
159+
prel.AddProperty("AndroidApplication", "True");
160+
prel.AddProperty("IntermediateOutputPath", monoAndroidResDirIntermediate + Path.DirectorySeparatorChar);
161+
prel.AddProperty("MonoAndroidResDirIntermediate", monoAndroidResDirIntermediate);
162+
prel.AddProperty("_AndroidPackage", "com.xamarin.sample");
163+
prel.AddProperty("MSBuildProjectFile", "project.csproj");
164+
165+
// First build with stage file
166+
prel.AddItem("GoogleServicesJson", googleServicesJsonPath1);
167+
AddCoreTargets(prel);
168+
169+
var project = new ProjectInstance(prel);
170+
var log = new MSBuildTestLogger();
171+
172+
var success = BuildProject(engine, project, "ProcessGoogleServicesJson", log);
173+
Assert.IsTrue(success);
174+
Assert.IsFalse(log.Events.Any(e => e.Message.Contains("ProcessGoogleServicesJson") && e.Message.Contains("skipped")));
175+
176+
// Verify the cache file was created
177+
var cacheFilePath = Path.Combine(monoAndroidResDirIntermediate, "project.csproj.GoogleServicesJson.cache");
178+
Assert.IsTrue(File.Exists(cacheFilePath), "Cache file should exist after first build");
179+
var firstHash = File.ReadAllText(cacheFilePath);
180+
181+
// Now rebuild with the same file - should skip
182+
engine.UnloadAllProjects();
183+
engine = new ProjectCollection();
184+
prel = ProjectRootElement.Create(Path.Combine(TempDir, "project.csproj"), engine);
185+
prel.AddProperty("AndroidApplication", "True");
186+
prel.AddProperty("IntermediateOutputPath", monoAndroidResDirIntermediate + Path.DirectorySeparatorChar);
187+
prel.AddProperty("MonoAndroidResDirIntermediate", monoAndroidResDirIntermediate);
188+
prel.AddProperty("_AndroidPackage", "com.xamarin.sample");
189+
prel.AddProperty("MSBuildProjectFile", "project.csproj");
190+
prel.AddItem("GoogleServicesJson", googleServicesJsonPath1);
191+
AddCoreTargets(prel);
192+
193+
project = new ProjectInstance(prel);
194+
log = new MSBuildTestLogger();
195+
196+
success = BuildProject(engine, project, "ProcessGoogleServicesJson", log);
197+
Assert.IsTrue(success);
198+
Assert.IsTrue(log.Events.Any(e => e.Message.Contains("ProcessGoogleServicesJson") && e.Message.Contains("skipped")),
199+
"Target should be skipped when inputs haven't changed");
200+
201+
// Now rebuild with a different file - should NOT skip
202+
engine.UnloadAllProjects();
203+
engine = new ProjectCollection();
204+
prel = ProjectRootElement.Create(Path.Combine(TempDir, "project.csproj"), engine);
205+
prel.AddProperty("AndroidApplication", "True");
206+
prel.AddProperty("IntermediateOutputPath", monoAndroidResDirIntermediate + Path.DirectorySeparatorChar);
207+
prel.AddProperty("MonoAndroidResDirIntermediate", monoAndroidResDirIntermediate);
208+
prel.AddProperty("_AndroidPackage", "com.xamarin.sample");
209+
prel.AddProperty("MSBuildProjectFile", "project.csproj");
210+
prel.AddItem("GoogleServicesJson", googleServicesJsonPath2); // Different file!
211+
AddCoreTargets(prel);
212+
213+
project = new ProjectInstance(prel);
214+
log = new MSBuildTestLogger();
215+
216+
success = BuildProject(engine, project, "ProcessGoogleServicesJson", log);
217+
Assert.IsTrue(success);
218+
Assert.IsFalse(log.Events.Any(e => e.Message.Contains("ProcessGoogleServicesJson") && e.Message.Contains("skipped")),
219+
"Target should NOT be skipped when GoogleServicesJson list changes");
220+
221+
// Verify the cache file has a different hash
222+
var secondHash = File.ReadAllText(cacheFilePath);
223+
Assert.AreNotEqual(firstHash, secondHash, "Hash should be different when input file changes");
224+
}
96225
}
97226
}

source/com.google.android.gms/play-services-basement/merge.targets

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
<ProcessGoogleServicesJsonResStringsPath>$(ProcessGoogleServicesJsonResDirPath)values\goog_svcs_json.xml</ProcessGoogleServicesJsonResStringsPath>
2727
<ProcessGoogleServicesJsonResXmlPath>$(ProcessGoogleServicesJsonResDirPath)xml\global_tracker.xml</ProcessGoogleServicesJsonResXmlPath>
2828
<ProcessGoogleServicesJsonStampPath>$(IntermediateOutputPath)googsvcsjson.stamp</ProcessGoogleServicesJsonStampPath>
29+
<ProcessGoogleServicesJsonCachePath>$(IntermediateOutputPath)$(MSBuildProjectFile).GoogleServicesJson.cache</ProcessGoogleServicesJsonCachePath>
2930
</PropertyGroup>
3031

3132
<PropertyGroup>
@@ -42,8 +43,28 @@
4243

4344
<!-- the build tasks -->
4445
<Target
45-
Name="SetupGoogleServicesJson"
46+
Name="_ComputeGoogleServicesJsonHash"
4647
Condition=" '@(GoogleServicesJson)' != '' AND '$(AndroidApplication)' == 'True'">
48+
<!-- Hash the GoogleServicesJson items to detect when the list or content changes -->
49+
<Hash ItemsToHash="@(GoogleServicesJson)">
50+
<Output TaskParameter="HashResult" PropertyName="_GoogleServicesJsonHash" />
51+
</Hash>
52+
<!-- Write the hash to a cache file; this file will be used as an input to ProcessGoogleServicesJson -->
53+
<WriteLinesToFile
54+
Lines="$(_GoogleServicesJsonHash)"
55+
File="$(ProcessGoogleServicesJsonCachePath)"
56+
Overwrite="True"
57+
WriteOnlyWhenDifferent="True"
58+
/>
59+
<ItemGroup>
60+
<FileWrites Include="$(ProcessGoogleServicesJsonCachePath)" />
61+
</ItemGroup>
62+
</Target>
63+
64+
<Target
65+
Name="SetupGoogleServicesJson"
66+
Condition=" '@(GoogleServicesJson)' != '' AND '$(AndroidApplication)' == 'True'"
67+
DependsOnTargets="_ComputeGoogleServicesJsonHash">
4768

4869
<ConvertToAbsolutePath Paths="$(ProcessGoogleServicesJsonResDirPath)">
4970
<Output TaskParameter="AbsolutePaths" PropertyName="ProcessGoogleServicesJsonResDirPathAbs"/>
@@ -56,7 +77,7 @@
5677
Condition=" '@(GoogleServicesJson)' != '' AND '$(AndroidApplication)' == 'True'"
5778
AfterTargets="_ValidateAndroidPackageProperties"
5879
DependsOnTargets="SetupGoogleServicesJson"
59-
Inputs="@(GoogleServicesJson)"
80+
Inputs="@(GoogleServicesJson);$(ProcessGoogleServicesJsonCachePath)"
6081
Outputs="$(IntermediateOutputPath)googsvcsjson.stamp">
6182

6283
<ProcessGoogleServicesJson
@@ -81,5 +102,6 @@
81102
<Target Name="_CleanProcessGoogleServicesJson">
82103
<RemoveDir Directories="$(ProcessGoogleServicesJsonResDirPath)" Condition="Exists ('$(ProcessGoogleServicesJsonResDirPath)' )" />
83104
<Delete Files="$(ProcessGoogleServicesJsonStampPath)" />
105+
<Delete Files="$(ProcessGoogleServicesJsonCachePath)" />
84106
</Target>
85107
</Project>

0 commit comments

Comments
 (0)