Skip to content

Commit 83ab07e

Browse files
author
Stewart Miles
committed
Moved embedded resource management into a shared module.
Added the EmbeddedResource module which moves the logic to extract DLL embedded resources out of GradleResolver so that it can be resused more easily by other modules. This also adds functionality to keep extracted resources up to date when the source assembly is updated and modifies zip extraction to conditionally extract files based upon modification time. Bug: 135269831, 135273820 Change-Id: I259743e4ae7475a05a5140f2bc9ad6099d40be16
1 parent 17898ec commit 83ab07e

File tree

5 files changed

+236
-61
lines changed

5 files changed

+236
-61
lines changed

source/PlayServicesResolver/PlayServicesResolver.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
<Compile Include="src\AndroidXmlDependencies.cs" />
6060
<Compile Include="src\CommandLine.cs" />
6161
<Compile Include="src\CommandLineDialog.cs" />
62+
<Compile Include="src\EmbeddedResource.cs" />
6263
<Compile Include="src\GradleResolver.cs" />
6364
<Compile Include="src\GradleTemplateResolver.cs" />
6465
<Compile Include="src\LocalMavenRepository.cs" />
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// <copyright file="EmbeddedResource.cs" company="Google Inc.">
2+
// Copyright (C) 2019 Google Inc. All Rights Reserved.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
// </copyright>
16+
17+
namespace Google {
18+
using System;
19+
using System.Collections.Generic;
20+
using System.IO;
21+
using System.Reflection;
22+
23+
/// <summary>
24+
/// Methods to manage assembly embedded resources.
25+
/// </summary>
26+
internal static class EmbeddedResource {
27+
28+
/// <summary>
29+
/// Extracted resource file information.
30+
/// </summary>
31+
private class ExtractedResource {
32+
33+
/// <summary>
34+
/// Assembly that contains the resource.
35+
/// </summary>
36+
private Assembly assembly;
37+
38+
/// <summary>
39+
/// Assembly modification time.
40+
/// </summary>
41+
private DateTime assemblyModificationTime;
42+
43+
/// <summary>
44+
/// Name of the extracted resource.
45+
/// </summary>
46+
private string resourceName;
47+
48+
/// <summary>
49+
/// Path to the extracted resource.
50+
/// </summary>
51+
private string path;
52+
53+
/// <summary>
54+
/// Construct an object to track an extracted resource.
55+
/// </summary>
56+
/// <param name="assembly">Assembly the resource was extracted from.</param>
57+
/// <param name="assemblyModificationTime">Last time the source assembly file was
58+
/// modified.</param>
59+
/// <param name="resourceName">Name of the resource in the assembly.</param>
60+
/// <param name="path">Path of the extracted resource.</param>
61+
public ExtractedResource(Assembly assembly, DateTime assemblyModificationTime,
62+
string resourceName, string path) {
63+
this.assembly = assembly;
64+
this.assemblyModificationTime = assemblyModificationTime;
65+
this.resourceName = resourceName;
66+
this.path = path;
67+
}
68+
69+
/// <summary>
70+
/// Name of the extracted resource.
71+
/// </summary>
72+
public string ResourceName { get { return resourceName; } }
73+
74+
/// <summary>
75+
/// Path of the extracted file.
76+
/// </summary>
77+
public string Path { get { return path; } }
78+
79+
/// <summary>
80+
/// Whether the extracted file is out of date.
81+
/// </summary>
82+
public bool OutOfDate {
83+
get {
84+
// If the source assembly is newer than the extracted file, the extracted file
85+
// is out of date.
86+
return !File.Exists(path) ||
87+
assemblyModificationTime.CompareTo(File.GetLastWriteTime(path)) > 0;
88+
}
89+
}
90+
91+
/// <summary>
92+
/// Extract the embedded resource to the Path creating intermediate directories
93+
/// if they're required.
94+
/// </summary>
95+
/// <param name="logger">Logger to log messages to.</param>
96+
/// <returns>true if successful, false otherwise.</returns>
97+
public bool Extract(Logger logger) {
98+
if (OutOfDate) {
99+
var stream = assembly.GetManifestResourceStream(resourceName);
100+
if (stream == null) {
101+
logger.Log(
102+
String.Format("Failed to find resource {0} in assembly {1}",
103+
ResourceName, assembly.FullName),
104+
level: LogLevel.Error);
105+
return false;
106+
}
107+
var data = new byte[stream.Length];
108+
stream.Read(data, 0, (int)stream.Length);
109+
try {
110+
Directory.CreateDirectory(System.IO.Path.GetDirectoryName(
111+
FileUtils.NormalizePathSeparators(Path)));
112+
File.WriteAllBytes(Path, data);
113+
} catch (IOException error) {
114+
logger.Log(
115+
String.Format("Failed to write resource {0} from assembly {1} to {2} " +
116+
"({3})", ResourceName, assembly.FullName, Path, error),
117+
level: LogLevel.Error);
118+
return false;
119+
}
120+
}
121+
return true;
122+
}
123+
}
124+
125+
// Cache of extracted resources by path. This is used to avoid file operations when
126+
// checking to see whether resources have already been extracted or are out of date.
127+
private static Dictionary<string, ExtractedResource> extractedResourceByPath =
128+
new Dictionary<string, ExtractedResource>();
129+
130+
/// <summary>
131+
/// Extract a list of embedded resources to the specified path creating intermediate
132+
/// directories if they're required.
133+
/// </summary>
134+
/// <param name="assembly">Assembly to extract resources from.</param>
135+
/// <param name="resourceNameToTargetPath">Each Key is the resource to extract and each
136+
/// Value is the path to extract to. If the resource name (Key) is null or empty, this
137+
/// method will attempt to extract a resource matching the filename component of the path.
138+
/// </param>
139+
/// <returns>true if successful, false otherwise.</returns>
140+
public static bool ExtractResources(
141+
Assembly assembly,
142+
IEnumerable<KeyValuePair<string, string>> resourceNameToTargetPaths,
143+
Logger logger) {
144+
bool allResourcesExtracted = true;
145+
var assemblyModificationTime = File.GetLastWriteTime(assembly.Location);
146+
foreach (var resourceNameToTargetPath in resourceNameToTargetPaths) {
147+
var targetPath = FileUtils.PosixPathSeparators(
148+
Path.GetFullPath(resourceNameToTargetPath.Value));
149+
var resourceName = resourceNameToTargetPath.Key;
150+
if (String.IsNullOrEmpty(resourceName)) {
151+
resourceName = Path.GetFileName(FileUtils.NormalizePathSeparators(targetPath));
152+
}
153+
ExtractedResource resourceToExtract;
154+
if (!extractedResourceByPath.TryGetValue(targetPath, out resourceToExtract)) {
155+
resourceToExtract = new ExtractedResource(assembly, assemblyModificationTime,
156+
resourceName, targetPath);
157+
}
158+
if (resourceToExtract.Extract(logger)) {
159+
extractedResourceByPath[targetPath] = resourceToExtract;
160+
} else {
161+
allResourcesExtracted = false;
162+
}
163+
}
164+
return allResourcesExtracted;
165+
}
166+
}
167+
}

source/PlayServicesResolver/src/GradleResolver.cs

Lines changed: 39 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,10 @@ private void GradleResolution(
671671
string destinationDirectory, string androidSdkPath,
672672
bool logErrorOnMissingArtifacts, bool closeWindowOnCompletion,
673673
System.Action<List<Dependency>> resolutionComplete) {
674+
// Get all dependencies.
675+
var allDependencies = PlayServicesSupport.GetAllDependencies();
676+
var allDependenciesList = new List<Dependency>(allDependencies.Values);
677+
674678
// Namespace for resources under the src/scripts directory embedded within this
675679
// assembly.
676680
const string EMBEDDED_RESOURCES_NAMESPACE = "PlayServicesResolver.scripts.";
@@ -680,30 +684,32 @@ private void GradleResolution(
680684
"gradlew.bat" : "gradlew"));
681685
var buildScript = Path.GetFullPath(Path.Combine(
682686
gradleBuildDirectory, EMBEDDED_RESOURCES_NAMESPACE + "download_artifacts.gradle"));
683-
// Get all dependencies.
684-
var allDependencies = PlayServicesSupport.GetAllDependencies();
685-
var allDependenciesList = new List<Dependency>(allDependencies.Values);
687+
var gradleTemplateZip = Path.Combine(
688+
gradleBuildDirectory, EMBEDDED_RESOURCES_NAMESPACE + "gradle-template.zip");
689+
690+
// Extract the gradle wrapper and build script.
691+
if (!EmbeddedResource.ExtractResources(
692+
typeof(GradleResolver).Assembly,
693+
new KeyValuePair<string, string>[] {
694+
new KeyValuePair<string, string>(null, gradleTemplateZip),
695+
new KeyValuePair<string, string>(null, buildScript),
696+
}, PlayServicesResolver.logger)) {
697+
PlayServicesResolver.Log(String.Format(
698+
"Failed to extract {0} and {1} from assembly {2}",
699+
gradleTemplateZip, buildScript, typeof(GradleResolver).Assembly.FullName),
700+
level: LogLevel.Error);
701+
resolutionComplete(allDependenciesList);
702+
return;
703+
}
686704

687705
// Extract Gradle wrapper and the build script to the build directory.
688-
if (!(Directory.Exists(gradleBuildDirectory) && File.Exists(gradleWrapper) &&
689-
File.Exists(buildScript))) {
690-
var gradleTemplateZip = Path.Combine(
691-
gradleBuildDirectory, EMBEDDED_RESOURCES_NAMESPACE + "gradle-template.zip");
692-
foreach (var resource in new [] { gradleTemplateZip, buildScript }) {
693-
ExtractResource(Path.GetFileName(resource), resource);
694-
}
695-
if (!PlayServicesResolver.ExtractZip(gradleTemplateZip, new [] {
696-
"gradle/wrapper/gradle-wrapper.jar",
697-
"gradle/wrapper/gradle-wrapper.properties",
698-
"gradlew",
699-
"gradlew.bat"}, gradleBuildDirectory)) {
700-
PlayServicesResolver.Log(
701-
String.Format("Unable to extract Gradle build component {0}\n\n" +
702-
"Resolution failed.", gradleTemplateZip),
703-
level: LogLevel.Error);
704-
resolutionComplete(allDependenciesList);
705-
return;
706-
}
706+
if (PlayServicesResolver.ExtractZip(
707+
gradleTemplateZip, new [] {
708+
"gradle/wrapper/gradle-wrapper.jar",
709+
"gradle/wrapper/gradle-wrapper.properties",
710+
"gradlew",
711+
"gradlew.bat"},
712+
gradleBuildDirectory, true)) {
707713
// Files extracted from the zip file don't have the executable bit set on some
708714
// platforms, so set it here.
709715
// Unfortunately, File.GetAccessControl() isn't implemented, so we'll use
@@ -723,6 +729,13 @@ private void GradleResolution(
723729
return;
724730
}
725731
}
732+
} else {
733+
PlayServicesResolver.Log(
734+
String.Format("Unable to extract Gradle build component {0}\n\n" +
735+
"Resolution failed.", gradleTemplateZip),
736+
level: LogLevel.Error);
737+
resolutionComplete(allDependenciesList);
738+
return;
726739
}
727740

728741
// Build array of repos to search, they're interleaved across all dependencies as the
@@ -1751,7 +1764,7 @@ internal virtual bool ShouldExplode(string aarPath) {
17511764
if (aarPath.EndsWith(".aar") &&
17521765
PlayServicesResolver.ExtractZip(
17531766
aarPath, new string[] {manifestFilename, "jni", classesFilename},
1754-
temporaryDirectory)) {
1767+
temporaryDirectory, false)) {
17551768
string manifestPath = Path.Combine(temporaryDirectory,
17561769
manifestFilename);
17571770
if (File.Exists(manifestPath)) {
@@ -1904,7 +1917,9 @@ internal static bool ProcessAar(string dir, string aarFile, bool antProject,
19041917
return false;
19051918
}
19061919
Directory.CreateDirectory(workingDir);
1907-
if (!PlayServicesResolver.ExtractZip(aarFile, null, workingDir)) return false;
1920+
if (!PlayServicesResolver.ExtractZip(aarFile, null, workingDir, false)) {
1921+
return false;
1922+
}
19081923
PlayServicesResolver.ReplaceVariablesInAndroidManifest(
19091924
Path.Combine(workingDir, "AndroidManifest.xml"),
19101925
PlayServicesResolver.GetAndroidApplicationId(),
@@ -2045,35 +2060,5 @@ internal static bool ProcessAar(string dir, string aarFile, bool antProject,
20452060
}
20462061
return true;
20472062
}
2048-
2049-
/// <summary>
2050-
/// Extract a list of embedded resources to the specified path creating intermediate
2051-
/// directories if they're required.
2052-
/// </summary>
2053-
/// <param name="resourceNameToTargetPath">Each Key is the resource to extract and each
2054-
/// Value is the path to extract to.</param>
2055-
protected static void ExtractResources(List<KeyValuePair<string, string>>
2056-
resourceNameToTargetPaths) {
2057-
foreach (var kv in resourceNameToTargetPaths) ExtractResource(kv.Key, kv.Value);
2058-
}
2059-
2060-
/// <summary>
2061-
/// Extract an embedded resource to the specified path creating intermediate directories
2062-
/// if they're required.
2063-
/// </summary>
2064-
/// <param name="resourceName">Name of the resource to extract.</param>
2065-
/// <param name="targetPath">Target path.</param>
2066-
protected static void ExtractResource(string resourceName, string targetPath) {
2067-
Directory.CreateDirectory(Path.GetDirectoryName(targetPath));
2068-
var stream = typeof(GradleResolver).Assembly.GetManifestResourceStream(resourceName);
2069-
if (stream == null) {
2070-
UnityEngine.Debug.LogError(String.Format("Failed to find resource {0} in assembly",
2071-
resourceName));
2072-
return;
2073-
}
2074-
var data = new byte[stream.Length];
2075-
stream.Read(data, 0, (int)stream.Length);
2076-
File.WriteAllBytes(targetPath, data);
2077-
}
20782063
}
20792064
}

source/PlayServicesResolver/src/PlayServicesResolver.cs

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2295,16 +2295,38 @@ internal static IEnumerable<string> ListZip(string zipFile) {
22952295
/// Extract a zip file to the specified directory.
22962296
/// </summary>
22972297
/// <param name="zipFile">Name of the zip file to extract.</param>
2298-
/// <param name="extractFilenames">List of files to extract from the archive. If this array
2299-
/// is empty or null all files are extracted.</param>
2298+
/// <param name="extractFilenames">Enumerable of files to extract from the archive. If this
2299+
/// array is empty or null all files are extracted.</param>
23002300
/// <param name="outputDirectory">Directory to extract the zip file to.</param>
2301+
/// <param name="update">If true, this will only extract the zip file if the target paths
2302+
/// are older than the source paths. If this is false or no extractFilenames are specified
2303+
/// this method will always extract files from the zip file overwriting the target
2304+
/// files.</param>
23012305
/// <returns>true if successful, false otherwise.</returns>
2302-
internal static bool ExtractZip(string zipFile, string[] extractFilenames,
2303-
string outputDirectory) {
2306+
internal static bool ExtractZip(string zipFile, IEnumerable<string> extractFilenames,
2307+
string outputDirectory, bool update) {
23042308
try {
23052309
string zipPath = Path.GetFullPath(zipFile);
2306-
string extractFilesArg = extractFilenames != null && extractFilenames.Length > 0 ?
2307-
String.Format("\"{0}\"", String.Join("\" \"", extractFilenames)) : "";
2310+
var zipFileModificationTime = File.GetLastWriteTime(zipPath);
2311+
string extractFilesArg = "";
2312+
if (extractFilenames != null) {
2313+
bool outOfDate = !update;
2314+
if (update) {
2315+
foreach (var filename in extractFilenames) {
2316+
var path = Path.Combine(outputDirectory, filename);
2317+
if (!File.Exists(path) ||
2318+
zipFileModificationTime.CompareTo(
2319+
File.GetLastWriteTime(path)) > 0) {
2320+
outOfDate = true;
2321+
break;
2322+
}
2323+
}
2324+
}
2325+
// If everything is up to date there is nothing to do.
2326+
if (!outOfDate) return true;
2327+
extractFilesArg = String.Format("\"{0}\"", String.Join("\" \"",
2328+
(new List<string>(extractFilenames)).ToArray()));
2329+
}
23082330
Log(String.Format("Extracting {0} ({1}) to {2}", zipFile, extractFilesArg,
23092331
outputDirectory), level: LogLevel.Verbose);
23102332
CommandLine.Result result = CommandLine.Run(

source/PlayServicesResolver/test/resolve_async/Assets/PlayServicesResolver/Editor/TestResolveAsync.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -992,7 +992,7 @@ private static string ExtractZip(string zipFile, List<string> failureMessages) {
992992
// ExtractZip is not part of the public API.
993993
bool successful = (bool)AndroidResolverClass.GetMethod(
994994
"ExtractZip", BindingFlags.Static | BindingFlags.NonPublic).Invoke(
995-
null, new object[]{ zipFile, null, outputDir });
995+
null, new object[]{ zipFile, null, outputDir, false });
996996
if (!successful) {
997997
failureMessages.Add(String.Format("Unable to extract {0} to {1}",
998998
zipFile, outputDir));

0 commit comments

Comments
 (0)