├── .gitattributes
├── .gitignore
├── .travis.yml
├── App.config
├── GitDependencies
├── DependencyManifest.cs
├── Log.cs
└── README.md
├── GitDepsPacker.csproj
├── LICENSE
├── Program.cs
├── Properties
└── AssemblyInfo.cs
├── README.md
├── StatHelper.cs
├── Wildcard.cs
└── WriteStream.cs
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.cs eol=native
2 | *.md eol=lf
3 |
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | obj
3 | *.suo
4 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: csharp
2 | sudo: false
3 | script:
4 | - xbuild /p:Configuration=Shipping GitDepsPacker.csproj
5 |
6 | before_deploy:
7 | - zip bin/GitDepsPacker-$TRAVIS_TAG.zip -j bin/Shipping/*.exe
8 |
9 | deploy:
10 | provider: releases
11 | api_key:
12 | secure: dUZVQD/r2258E/gEX/Lpw6FQsCPka2O8yV88FoiRDT3LkzPH8W9vreOPMAgWFY6elV2PdrP6F7EfcD2Pok0r/Y58YRMETdbt0R7KSulxDro+qj9Rm3Gw0xVi5wQ57XYu1s9HcSk/Cb9GLI/BWMRUbDZT5v/VUpDUUu5R445C0hr6oSmfjmSbHgNx71pC7XD4sQQ7w3Yru0bbFFlwsfYCu7xsGUnmAa0Y2sjJUfYNtlIkNd0Jvox2Cz+YevnkhMOnBKEQl7tJmcwtC/koPl8MTKShA5OlMguRHiiGLTAbqVhc3a3xPz/W9yiKtVoMCYBEhpF+53YWxCfL8LEBmTCby5dWbfFg/SAlrCUeY9pDV6XpfcZHg6qZZkG2yG2drMp/7zlrJQ50y0tXuxBattOVJ2tTNKeotBDqO13w5wAMeda/k36aPO7s99ElTfRJRUVzka4usOPguWj8pRYLNKcGz6JL80JQFxzeh94u+VR8LHogEKnanl8EeHiIe1mlxBjo6boP2sgNzEMLOZfUsXhpPqEC+cYoJCX+EUWp4PVZpAJcOoLpoRyXbOfSmWsZYndwfOOGtmmc7Jw+0YMCd8fkXA8b6DkyBtCeorP+GavAXBn7dOubWNAXEE7t4Bbq3VY+SF9mhXP4C5EtIsO/IjchR5JCw3u6QiUTEoXTGyYTFOM=
13 | file_glob: true
14 | file: bin/*.zip
15 | skip_cleanup: true
16 | on:
17 | tags: true
18 | all_branches: true
19 |
--------------------------------------------------------------------------------
/App.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/GitDependencies/DependencyManifest.cs:
--------------------------------------------------------------------------------
1 | // Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Diagnostics;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Text;
9 | using System.Threading.Tasks;
10 | using System.Xml.Serialization;
11 | using System.ComponentModel;
12 |
13 | namespace GitDependencies
14 | {
15 | [DebuggerDisplay("{Name}")]
16 | public class DependencyFile
17 | {
18 | [XmlAttribute]
19 | public string Name;
20 |
21 | [XmlAttribute]
22 | public string Hash;
23 |
24 | [XmlAttribute]
25 | [DefaultValue(false)]
26 | public bool IsExecutable;
27 | }
28 |
29 | [DebuggerDisplay("{Hash}")]
30 | public class DependencyBlob
31 | {
32 | [XmlAttribute]
33 | public string Hash;
34 |
35 | [XmlAttribute]
36 | public long Size;
37 |
38 | [XmlAttribute]
39 | public string PackHash;
40 |
41 | [XmlAttribute]
42 | public long PackOffset;
43 | }
44 |
45 | [DebuggerDisplay("{Hash}")]
46 | public class DependencyPack
47 | {
48 | [XmlAttribute]
49 | public string Hash;
50 |
51 | [XmlAttribute]
52 | public long Size;
53 |
54 | [XmlAttribute]
55 | public long CompressedSize;
56 |
57 | [XmlAttribute]
58 | public string RemotePath;
59 | }
60 |
61 | public class DependencyManifest
62 | {
63 | [XmlAttribute]
64 | public string BaseUrl = "http://cdn.unrealengine.com/dependencies";
65 |
66 | [XmlAttribute]
67 | [DefaultValue(false)]
68 | public bool IgnoreProxy;
69 |
70 | [XmlArrayItem("File")]
71 | public DependencyFile[] Files = new DependencyFile[0];
72 |
73 | [XmlArrayItem("Blob")]
74 | public DependencyBlob[] Blobs = new DependencyBlob[0];
75 |
76 | [XmlArrayItem("Pack")]
77 | public DependencyPack[] Packs = new DependencyPack[0];
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/GitDependencies/Log.cs:
--------------------------------------------------------------------------------
1 | // Copyright 1998-2015 Epic Games, Inc. All Rights Reserved.
2 |
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 | using System.Text;
7 | using System.Threading.Tasks;
8 |
9 | namespace GitDependencies
10 | {
11 | static class Log
12 | {
13 | static string CurrentStatus = "";
14 |
15 | public static void WriteLine()
16 | {
17 | FlushStatus();
18 | Console.WriteLine();
19 | }
20 |
21 | public static void WriteLine(string Line)
22 | {
23 | FlushStatus();
24 | Console.WriteLine(Line);
25 | }
26 |
27 | public static void WriteLine(string Format, params object[] Args)
28 | {
29 | FlushStatus();
30 | Console.WriteLine(Format, Args);
31 | }
32 |
33 | public static void WriteError(string Format, params object[] Args)
34 | {
35 | FlushStatus();
36 | Console.ForegroundColor = ConsoleColor.Red;
37 | Console.WriteLine(Format, Args);
38 | Console.ResetColor();
39 | }
40 |
41 | public static void WriteStatus(string Status)
42 | {
43 | // If the status is larger than the console width, truncate it
44 | string NewStatus = Status;
45 | try
46 | {
47 | int Width = Console.BufferWidth;
48 | if(NewStatus.Length >= Width)
49 | {
50 | NewStatus = NewStatus.Substring(0, Width - 1);
51 | }
52 | }
53 | catch(Exception)
54 | {
55 | }
56 |
57 | // Write the new status, and clear any space after the end of the string if it's shorter
58 | Console.Write("\r" + NewStatus);
59 | if(NewStatus.Length < CurrentStatus.Length)
60 | {
61 | Console.Write(new string(' ', CurrentStatus.Length - NewStatus.Length) + "\r" + NewStatus);
62 | }
63 | CurrentStatus = NewStatus;
64 | }
65 |
66 | public static void WriteStatus(string Format, params object[] Args)
67 | {
68 | WriteStatus(String.Format(Format, Args));
69 | }
70 |
71 | public static void FlushStatus()
72 | {
73 | if(CurrentStatus.Length > 0)
74 | {
75 | Console.WriteLine();
76 | CurrentStatus = "";
77 | }
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/GitDependencies/README.md:
--------------------------------------------------------------------------------
1 | # GitDependencies
2 |
3 | This directory has some files copied from ```Engine/Source/Programs/GitDependencies``` without any changes.
--------------------------------------------------------------------------------
/GitDepsPacker.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Debug
6 | AnyCPU
7 | Exe
8 | Properties
9 | GitDepsPacker
10 | GitDepsPacker
11 | v4.5
12 | 512
13 | {D674D12B-24F3-43A7-B7B1-125412CC9E33}
14 |
15 |
16 | AnyCPU
17 | true
18 | full
19 | false
20 | ..\..\..\Binaries\DotNET\
21 | DEBUG;TRACE
22 | prompt
23 | 4
24 |
25 |
26 | AnyCPU
27 | pdbonly
28 | false
29 | ..\..\..\Binaries\DotNET\
30 | TRACE
31 | prompt
32 | 4
33 |
34 |
35 | bin\Shipping\
36 | TRACE
37 | pdbonly
38 | AnyCPU
39 | prompt
40 | MinimumRecommendedRules.ruleset
41 | true
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | GitDependencies\DependencyManifest.cs
55 |
56 |
57 | GitDependencies\Log.cs
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
79 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 Artem V. Navrotskiy
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Collections.Generic;
4 | using System.ComponentModel;
5 | using System.IO;
6 | using System.IO.Compression;
7 | using System.Linq;
8 | using System.Net;
9 | using System.Reflection;
10 | using System.Security.Cryptography;
11 | using System.Text;
12 | using System.Threading;
13 | using System.Xml.Serialization;
14 | using GitDependencies;
15 | using System.Diagnostics;
16 |
17 | namespace GitDepsPacker
18 | {
19 | struct Parameters
20 | {
21 | // Specifies the path of the package root
22 | public string RootPath;
23 | // Output file name
24 | public string Target;
25 | // Directory for created UEPACK files
26 | public string Storage;
27 | // Base url of storage directory to gitdeps.xml file
28 | public string BaseUrl;
29 | // Remove path
30 | public string RemotePath;
31 | // Ignore proxy flag to gitdeps.xml file
32 | public bool IgnoreProxy;
33 | // Ignore git-tracked files
34 | public bool IgnoreGit;
35 | // Directory/file with ingnored gitdeps.xml files
36 | public List Ignore;
37 | // Directory/file with patched gitdeps.xml files
38 | public List Patch;
39 | // Directory/file with already created gitdeps.xml file for UEPACK reuse
40 | public List Reuse;
41 | // Optimal UEPACK size in bytes
42 | public int Optimal;
43 | // Workder thread count.
44 | public int Threads;
45 | // Wildcards.
46 | public List Wildcards;
47 | }
48 |
49 | class Program
50 | {
51 | private static byte[] Signature = Encoding.UTF8.GetBytes("UEPACK00");
52 |
53 | static int Main(string[] Args)
54 | {
55 | Parameters Params;
56 | // Build the argument list. Remove any double-hyphens from the start of arguments for conformity with other Epic tools.
57 | Params.Wildcards = new List();
58 | List ArgsList = new List();
59 | foreach (string Arg in Args)
60 | {
61 | if (Arg.StartsWith("-"))
62 | {
63 | ArgsList.Add(Arg.StartsWith("--") ? Arg.Substring(1) : Arg);
64 | }
65 | else
66 | {
67 | Params.Wildcards.Add(new Wildcard(Arg));
68 | }
69 | }
70 |
71 | // Parse the parameters
72 | Params.RootPath = ParseParameter(ArgsList, "-root=", Path.GetFullPath(Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "../../..")));
73 | Params.Target = ParseParameter(ArgsList, "-target=", Path.GetFullPath(Path.Combine(Params.RootPath, "../uepacks.gitdeps.xml")));
74 | Params.Storage = ParseParameter(ArgsList, "-storage=", Path.GetFullPath(Path.Combine(Params.RootPath, "../uepacks/")));
75 | Params.BaseUrl = ParseParameter(ArgsList, "-base-url=", null);
76 | Params.RemotePath = ParseParameter(ArgsList, "-remote-path=", null);
77 | Params.IgnoreProxy = ParseSwitch(ArgsList, "-ignore-proxy");
78 | Params.IgnoreGit = ParseSwitch(ArgsList, "-ignore-git");
79 | Params.Ignore = ParseFiles(ArgsList, "-ignore=", "*.gitdeps.xml", null);
80 | Params.Patch = ParseFiles(ArgsList, "-patch=", "*.gitdeps.xml", Path.GetFullPath(Path.Combine(Params.RootPath, "Engine/Build")));
81 | Params.Reuse = ParseFiles(ArgsList, "-reuse=", "*.gitdeps.xml", null);
82 | Params.Threads = Math.Max(int.Parse(ParseParameter(ArgsList, "-threads=", "4")), 1);
83 | Params.Optimal = int.Parse(ParseParameter(ArgsList, "-optimal=", "10")) * 1024 * 1024;
84 |
85 | // If there are any more parameters, print an error
86 | bool bHelp = ParseSwitch(ArgsList, "-help");
87 | foreach (string RemainingArg in ArgsList)
88 | {
89 | Log.WriteLine("Invalid command line parameter: {0}", RemainingArg);
90 | Log.WriteLine();
91 | bHelp = true;
92 | }
93 |
94 | // Print the help message
95 | if (bHelp)
96 | {
97 | Log.WriteLine("Usage:");
98 | Log.WriteLine(" GitDepsPacker [options] [wilcards]");
99 | Log.WriteLine();
100 | Log.WriteLine("Options:");
101 | Log.WriteLine(" --root= Specifies the path of the package root");
102 | Log.WriteLine(" --target= Output file name");
103 | Log.WriteLine(" --storage= Directory for created UEPACK files");
104 | Log.WriteLine(" --base-url= Add base url of storage directory to *.gitdeps.xml file");
105 | Log.WriteLine(" --remote-path= Remote path of created packs");
106 | Log.WriteLine(" --ignore-proxy Add ignore proxy flag to *.gitdeps.xml file");
107 | Log.WriteLine(" --ignore= Directory/file with already created gitdeps.xml file for ignore file list");
108 | Log.WriteLine(" --patch= Directory/file with already created gitdeps.xml file for patching for remove changed file list");
109 | Log.WriteLine(" --reuse= Directory/file with already created gitdeps.xml file for UEPACK reuse");
110 | Log.WriteLine(" --optimal= Optimal UEPACK size");
111 | Log.WriteLine(" --threads=X Use X threads");
112 | Log.WriteLine(" --ignore-git Ignore files, tracked by git");
113 | Log.WriteLine();
114 | Log.WriteLine("Wilcards example:");
115 | Log.WriteLine(" **/Binaries/ Include all files in all subdirectories named Binaries");
116 | Log.WriteLine(" Engine/Intermediate/Build/Win64/Inc/ Include all files in Engine/Intermediate/Build/Win64/Inc directory");
117 | Log.WriteLine(" Engine/Intermediate/Build/Win64/UE4Editor/**/*.lib Include all *.lib files in Engine/Intermediate/Build/Win64/UE4Editor directory");
118 | Log.WriteLine(" !**/*.pdb Exclude all *.pdb files in all subdirectories");
119 | return 1;
120 | }
121 |
122 | Log.WriteLine("Current options:");
123 | Log.WriteLine(" Content root: {0}", Params.RootPath);
124 | Log.WriteLine(" Target .gitdeps.xml file: {0}", Params.Target);
125 | Log.WriteLine(" Output UEPACK storage path: {0}", Params.Storage);
126 | Log.WriteLine(" CDN base url: {0}", Params.BaseUrl == null ? "default" : Params.BaseUrl);
127 | Log.WriteLine(" Remote path: {0}", Params.RemotePath == null ? "none" : Params.RemotePath);
128 | Log.WriteLine(" Ignore proxy flag: {0}", Params.IgnoreProxy);
129 | LogFiles(" Patch already packed file list in:", Params.Patch);
130 | LogFiles(" Ignore already packed files from:", Params.Ignore);
131 | LogFiles(" Reuse UEPACK files from:", Params.Reuse);
132 | Log.WriteLine(" Ignore: already packed files from:" + (Params.Ignore.Count == 0 ? " none" : ""));
133 | foreach (string Item in Params.Ignore)
134 | {
135 | Log.WriteLine(" - {0}", Item);
136 | }
137 | Log.WriteLine(" Optimal pack size: {0} MB", Params.Optimal / (1024 * 1024));
138 | Log.WriteLine(" Worker threads: {0}", Params.Threads);
139 | Log.WriteLine();
140 |
141 | // Register a delegate to clear the status text if we use ctrl-c to quit
142 | Console.CancelKeyPress += delegate { Log.FlushStatus(); };
143 | return DoWork(Params);
144 | }
145 |
146 | private static void LogFiles(string Message, ICollection Files)
147 | {
148 | Log.WriteLine(Message + (Files.Count == 0 ? " none" : ""));
149 | foreach (string Item in Files)
150 | {
151 | Log.WriteLine(" - {0}", Item);
152 | }
153 | }
154 |
155 | private static int DoWork(Parameters Params)
156 | {
157 | // Find all target files.
158 | Log.WriteLine("Search files...");
159 | ISet TargetFiles = new HashSet();
160 | FindAllFiles(TargetFiles, Path.GetFullPath(Params.RootPath), "", Params.Wildcards);
161 | // Remove Git files.
162 | if (Params.IgnoreGit)
163 | {
164 | Log.WriteLine("Remove git-tracked files...");
165 | TargetFiles = RemoveGitFiles(Path.GetFullPath(Params.RootPath), TargetFiles);
166 | }
167 | // Remove ignored files.
168 | Log.WriteLine("Remove ignored files...");
169 | foreach (string Item in Params.Ignore)
170 | {
171 | Log.WriteLine(" " + Item);
172 | RemoveIgnoreFiles(Item, TargetFiles);
173 | }
174 | // Calcuate blob information.
175 | Log.WriteLine("Calculate blob information...");
176 | ConcurrentDictionary DepFiles = new ConcurrentDictionary(); // path -> file
177 | ConcurrentDictionary DepBlobs = new ConcurrentDictionary(); // hash -> blob
178 | GenerateBlobList(Params.Threads, Params.RootPath, TargetFiles, DepFiles, DepBlobs);
179 | // Patch exists manifest files.
180 | Log.WriteLine("Patch exists manifest files...");
181 | foreach (string Item in Params.Patch)
182 | {
183 | Log.WriteLine(" " + Item);
184 | RemoveRepackedFiles(Item, DepFiles);
185 | }
186 | // Add blob reuse information.
187 | Log.WriteLine("Reuse manifest pack files...");
188 | ConcurrentDictionary DepPacks = new ConcurrentDictionary(); // hash -> pack
189 | foreach (string Item in Params.Reuse)
190 | {
191 | Log.WriteLine(" " + Item);
192 | UpdateReusedPacks(Item, DepBlobs, DepPacks);
193 | }
194 | // Generate pack files.
195 | Log.WriteLine("Generate pack files...");
196 | GeneratePackFiles(Params, DepFiles, DepBlobs, DepPacks);
197 | // Write manifest file
198 | Log.WriteLine("Write manifest file");
199 | GenerateManifest(Params, DepFiles, DepBlobs, DepPacks);
200 | return 0;
201 | }
202 |
203 | private static void GenerateBlobList(int ThreadCount, string Root, ISet Files, ConcurrentDictionary DepFiles, ConcurrentDictionary DepBlobs)
204 | {
205 | // Initialize StatFileHelper in main thread for workaroung mono non-theadsafe assembly loading.
206 | StatFileHelper.IsExecutalbe(".");
207 |
208 | ConcurrentQueue FilesQueue = new ConcurrentQueue(Files);
209 | Thread[] Threads = new Thread[ThreadCount];
210 | for (int i = 0; i < Threads.Length; ++i)
211 | {
212 | Threads[i] = new Thread(x => GenerateBlobListThread(Root, FilesQueue, DepFiles, DepBlobs));
213 | Threads[i].Start();
214 | }
215 | foreach (Thread T in Threads)
216 | {
217 | T.Join();
218 | }
219 | }
220 |
221 | private static void GenerateBlobListThread(string Root, ConcurrentQueue FilesQueue, ConcurrentDictionary DepFiles, ConcurrentDictionary DepBlobs)
222 | {
223 | string PackFile;
224 | while (FilesQueue.TryDequeue(out PackFile))
225 | {
226 | string FullPath = Path.Combine(Root, PackFile);
227 | long FileSize;
228 | // Add File info.
229 | DependencyFile DepFile = new DependencyFile();
230 | DepFile.IsExecutable = StatFileHelper.IsExecutalbe(FullPath);
231 | DepFile.Name = PackFile;
232 | DepFile.Hash = ComputeHashForFile(FullPath, out FileSize);
233 | if (DepFiles.TryAdd(PackFile, DepFile))
234 | {
235 | // Add Blob info.
236 | DependencyBlob DepBlob = new DependencyBlob();
237 | DepBlob.Hash = DepFile.Hash;
238 | DepBlob.Size = FileSize;
239 | DepBlobs.TryAdd(DepBlob.Hash, DepBlob);
240 | }
241 | }
242 | }
243 |
244 | public static string ComputeHashForFile(string FileName, out long FileSize)
245 | {
246 | using (FileStream InputStream = File.OpenRead(FileName))
247 | {
248 | byte[] Hash = new SHA1CryptoServiceProvider().ComputeHash(InputStream);
249 | FileSize = InputStream.Position;
250 | return BitConverter.ToString(Hash).ToLower().Replace("-", "");
251 | }
252 | }
253 |
254 | public static void ReadXmlObject(string FileName, out T NewObject)
255 | {
256 | XmlSerializer Serializer = new XmlSerializer(typeof(T));
257 | using (StreamReader Reader = new StreamReader(FileName))
258 | {
259 | NewObject = (T)Serializer.Deserialize(Reader);
260 | }
261 | }
262 | public static void WriteXmlObject(string FileName, T XmlObject)
263 | {
264 | XmlSerializer Serializer = new XmlSerializer(typeof(T));
265 | using (StreamWriter Writer = new StreamWriter(FileName))
266 | {
267 | Serializer.Serialize(Writer, XmlObject);
268 | }
269 | }
270 |
271 | private static void GenerateManifest(Parameters Params, IDictionary DepFiles, IDictionary DepBlobs, IDictionary DepPacks)
272 | {
273 | DependencyManifest Manifest = new DependencyManifest();
274 | IDictionary Files = new SortedDictionary();
275 | IDictionary Blobs = new SortedDictionary();
276 | IDictionary Packs = new SortedDictionary();
277 | foreach (DependencyFile DepFile in DepFiles.Values)
278 | {
279 | Files.Add(DepFile.Name, DepFile);
280 | DependencyBlob DepBlob = DepBlobs[DepFile.Hash];
281 | if (!Blobs.ContainsKey(DepBlob.Hash))
282 | {
283 | Blobs.Add(DepBlob.Hash, DepBlob);
284 | DependencyPack DepPack = DepPacks[DepBlob.PackHash];
285 | if (!Packs.ContainsKey(DepPack.Hash))
286 | {
287 | Packs.Add(DepPack.Hash, DepPack);
288 | }
289 | }
290 | }
291 |
292 | Manifest.BaseUrl = Params.BaseUrl;
293 | Manifest.IgnoreProxy = Params.IgnoreProxy;
294 | Manifest.Files = Files.Values.ToArray();
295 | Manifest.Blobs = Blobs.Values.ToArray();
296 | Manifest.Packs = Packs.Values.ToArray();
297 | WriteXmlObject(Params.Target, Manifest);
298 | }
299 |
300 | private static void GeneratePackFiles(Parameters Params, IDictionary DepFiles, IDictionary DepBlobs, ConcurrentDictionary DepPacks)
301 | {
302 | // Collect mapping from hash to file.
303 | IDictionary BlobToFile = new Dictionary();
304 | foreach (DependencyFile DepFile in DepFiles.Values)
305 | {
306 | if (!BlobToFile.ContainsKey(DepFile.Hash))
307 | {
308 | BlobToFile.Add(DepFile.Hash, Path.Combine(Params.RootPath, DepFile.Name));
309 | }
310 | }
311 | // Collect unpacked blobs.
312 | List UnpackedBlobs = new List();
313 | foreach (DependencyBlob DepBlob in DepBlobs.Values)
314 | {
315 | if ((DepBlob.PackHash == null) && BlobToFile.ContainsKey(DepBlob.Hash))
316 | {
317 | UnpackedBlobs.Add(DepBlob);
318 | }
319 | }
320 | // Sort by size (bigger first).
321 | UnpackedBlobs.Sort(CompareBlobsBySize);
322 | // Create pack files.
323 | if (!Directory.Exists(Params.Storage))
324 | {
325 | Directory.CreateDirectory(Params.Storage);
326 | }
327 | // Pack in some threads.
328 | ConcurrentQueue BlobsQueue = new ConcurrentQueue(UnpackedBlobs);
329 | Thread[] Threads = new Thread[Params.Threads];
330 | for (int i = 0; i < Threads.Length; ++i)
331 | {
332 | Threads[i] = new Thread(x => GeneratePackFilesThread(Params, BlobsQueue, BlobToFile, DepPacks));
333 | Threads[i].Start();
334 | }
335 | foreach (Thread T in Threads)
336 | {
337 | T.Join();
338 | }
339 | }
340 |
341 | private static void GeneratePackFilesThread(Parameters Params, ConcurrentQueue BlobsQueue, IDictionary BlobToFile, ConcurrentDictionary DepPacks)
342 | {
343 | while (true)
344 | {
345 | DependencyBlob DepBlob;
346 | if (!BlobsQueue.TryDequeue(out DepBlob))
347 | {
348 | return;
349 | }
350 | String TempPath = Path.Combine(Params.Storage, "." + Guid.NewGuid().ToString() + ".tmp");
351 | try
352 | {
353 | List PackedBlobs = new List();
354 | DependencyPack DepPack = new DependencyPack();
355 | using (FileStream Stream = File.Create(TempPath))
356 | {
357 | SHA1Managed Hasher = new SHA1Managed();
358 | using (GZipStream GZip = new GZipStream(Stream, CompressionMode.Compress, true))
359 | {
360 | CryptoStream HashStream = new CryptoStream(GZip, Hasher, CryptoStreamMode.Write);
361 | Stream PackStream = new WriteStream(HashStream);
362 | PackStream.Write(Signature, 0, Signature.Length);
363 | while (true)
364 | {
365 | PackedBlobs.Add(DepBlob);
366 | DepBlob.PackOffset = PackStream.Position;
367 | using (FileStream BlobFile = File.Open(BlobToFile[DepBlob.Hash], FileMode.Open, FileAccess.Read, FileShare.Read))
368 | {
369 | Log.WriteLine("Copying file: {0}", BlobToFile[DepBlob.Hash]);
370 |
371 | BlobFile.CopyTo(PackStream);
372 | }
373 | if ((Stream.Position > Params.Optimal) || (!BlobsQueue.TryDequeue(out DepBlob)))
374 | {
375 | break;
376 | }
377 | }
378 | DepPack.Size = PackStream.Position;
379 | HashStream.FlushFinalBlock();
380 | }
381 | DepPack.Hash = BitConverter.ToString(Hasher.Hash).ToLower().Replace("-", "");
382 | DepPack.CompressedSize = Stream.Position;
383 | }
384 | string PackFile = Path.Combine(Params.Storage, DepPack.Hash);
385 | if (!File.Exists(PackFile))
386 | {
387 | Log.WriteLine("Moving File: {0}", PackFile);
388 | File.Move(TempPath, PackFile);
389 | }
390 | foreach (DependencyBlob Blob in PackedBlobs)
391 | {
392 | Blob.PackHash = DepPack.Hash;
393 | }
394 | DepPack.RemotePath = Params.RemotePath;
395 | DepPacks.TryAdd(DepPack.Hash, DepPack);
396 | }
397 | finally
398 | {
399 | if (File.Exists(TempPath))
400 | {
401 | File.Delete(TempPath);
402 | }
403 | }
404 | }
405 | }
406 |
407 | private static int CompareBlobsBySize(DependencyBlob A, DependencyBlob B)
408 | {
409 | return (A.Size == B.Size) ? A.Hash.CompareTo(B.Hash) : B.Size.CompareTo(A.Size);
410 | }
411 |
412 | private static void UpdateReusedPacks(string ManifestFile, IDictionary DepBlobs, IDictionary DepPacks)
413 | {
414 | Log.WriteLine("UpdateReusedPacks: {0}", ManifestFile);
415 | DependencyManifest Manifest;
416 | ReadXmlObject(ManifestFile, out Manifest);
417 | ISet Packs = new HashSet();
418 | foreach (DependencyBlob Item in Manifest.Blobs)
419 | {
420 | DependencyBlob DepBlob;
421 | if (DepBlobs.TryGetValue(Item.Hash, out DepBlob) && (DepBlob.PackHash == null))
422 | {
423 | DepBlobs[Item.Hash] = Item;
424 | Packs.Add(Item.PackHash);
425 | }
426 | }
427 | foreach (DependencyPack Item in Manifest.Packs)
428 | {
429 | if (Packs.Remove(Item.Hash))
430 | {
431 | if (!DepPacks.ContainsKey(Item.Hash))
432 | {
433 | DepPacks.Add(Item.Hash, Item);
434 | }
435 | }
436 | }
437 | if (Packs.Count != 0)
438 | {
439 | throw new InvalidDataException("Found broken manifest file: " + ManifestFile);
440 | }
441 | }
442 |
443 | private static void RemoveIgnoreFiles(string ManifestFile, ISet TargetFiles)
444 | {
445 | DependencyManifest Manifest;
446 | ReadXmlObject(ManifestFile, out Manifest);
447 | foreach (DependencyFile Item in Manifest.Files)
448 | {
449 | TargetFiles.Remove(Item.Name);
450 | }
451 | }
452 |
453 | private static string FindExeFromPath(string ExeName, string ExpectedPathSubstring = null)
454 | {
455 | if (File.Exists(ExeName))
456 | {
457 | return Path.GetFullPath(ExeName);
458 | }
459 |
460 | foreach (string BasePath in Environment.GetEnvironmentVariable("PATH").Split(Path.PathSeparator))
461 | {
462 | var FullPath = Path.Combine(BasePath, ExeName);
463 | if (ExpectedPathSubstring == null || FullPath.IndexOf(ExpectedPathSubstring, StringComparison.InvariantCultureIgnoreCase) != -1)
464 | {
465 | if (File.Exists(FullPath))
466 | {
467 | return FullPath;
468 | }
469 | }
470 | }
471 |
472 | return null;
473 | }
474 |
475 | private static ISet RemoveGitFiles(string RootPath, ISet FoundFiles)
476 | {
477 | string git = "";
478 |
479 | if (Environment.OSVersion.Platform == System.PlatformID.Unix)
480 | {
481 | git = FindExeFromPath ("git");
482 | }
483 | else
484 | {
485 | git = FindExeFromPath("git.exe");
486 | }
487 |
488 | if (git == null)
489 | {
490 | throw new FileNotFoundException("Can't get find git executable");
491 | }
492 | ProcessStartInfo start = new ProcessStartInfo();
493 | start.Arguments = "status --untracked-files=all --short --ignored .";
494 | start.FileName = git;
495 | start.WorkingDirectory = RootPath;
496 | start.UseShellExecute = false;
497 | start.WindowStyle = ProcessWindowStyle.Hidden;
498 | start.CreateNoWindow = true;
499 | start.RedirectStandardOutput = true;
500 |
501 | // Run the external process & wait for it to finish
502 | ISet TargetFiles = new HashSet();
503 | using (Process proc = Process.Start(start))
504 | {
505 | while (true)
506 | {
507 | string line = proc.StandardOutput.ReadLine();
508 | if (line == null) break;
509 | if ((line.StartsWith("!") || line.StartsWith("?")) && line.Length > 3)
510 | {
511 | string path = line.Substring(3);
512 | if (FoundFiles.Contains(path))
513 | {
514 | TargetFiles.Add(path);
515 | }
516 | }
517 | }
518 |
519 | proc.WaitForExit();
520 |
521 | // Retrieve the app's exit code
522 | int exitCode = proc.ExitCode;
523 | if (exitCode != 0)
524 | {
525 | throw new Exception("Can't get git unracked files list");
526 | }
527 | }
528 | return TargetFiles;
529 | }
530 |
531 | private static void RemoveRepackedFiles(string ManifestFile, IDictionary DepFiles)
532 | {
533 | DependencyManifest Manifest;
534 | ReadXmlObject(ManifestFile, out Manifest);
535 |
536 | List NewFiles = new List();
537 | foreach (DependencyFile Item in Manifest.Files)
538 | {
539 | DependencyFile DepFile;
540 | if (DepFiles.TryGetValue(Item.Name, out DepFile))
541 | {
542 | if (DepFile.Hash == Item.Hash)
543 | {
544 | // File not changed - do not repack.
545 | DepFiles.Remove(Item.Name);
546 | NewFiles.Add(Item);
547 | }
548 | }
549 | else
550 | {
551 | NewFiles.Add(Item);
552 | }
553 | }
554 | if (Manifest.Files.Length != NewFiles.Count)
555 | {
556 | Manifest.Files = NewFiles.ToArray();
557 | WriteXmlObject(ManifestFile, Manifest);
558 | }
559 | }
560 |
561 | static void FindAllFiles(ISet TargetFiles, string RootPath, string SubPath, ICollection Wildcards)
562 | {
563 | foreach (string FullName in Directory.EnumerateFiles(Path.Combine(RootPath, SubPath)))
564 | {
565 | string FileName = Path.GetFileName(FullName);
566 | string PackName = SubPath.Length > 0 ? SubPath + '/' + FileName : FileName;
567 | if (IsMatchWildcards(PackName, Wildcards, true))
568 | {
569 | TargetFiles.Add(PackName);
570 | }
571 | }
572 | foreach (string FullName in Directory.EnumerateDirectories(Path.Combine(RootPath, SubPath)))
573 | {
574 | string FileName = Path.GetFileName(FullName);
575 | string PackName = SubPath.Length > 0 ? SubPath + '/' + FileName : FileName;
576 | if (IsMatchWildcards(PackName, Wildcards, false))
577 | {
578 | FindAllFiles(TargetFiles, RootPath, PackName, Wildcards);
579 | }
580 | }
581 | }
582 |
583 | static bool IsMatchWildcards(string PackName, ICollection Wildcards, bool FilePath)
584 | {
585 | if (Wildcards.Count == 0) return true;
586 | bool Found = false;
587 | foreach (Wildcard Item in Wildcards)
588 | {
589 | if ((Found == Item.Exclude) && Item.IsMatched(PackName, FilePath))
590 | {
591 | Found = !Found;
592 | }
593 | }
594 | return Found;
595 | }
596 |
597 | static List ParseFiles(List ArgsList, string Name, string DefaultMask, string DefaultPath)
598 | {
599 | List Params = new List(ParseParameters(ArgsList, Name));
600 | if (Params.Count() == 0 && DefaultPath != null)
601 | {
602 | Params.Add(DefaultPath);
603 | }
604 | List Result = new List();
605 | foreach (string Item in Params)
606 | {
607 | if (Item.Length == 0)
608 | {
609 | }
610 | else if (Directory.Exists(Item))
611 | {
612 | Result.AddRange(FindFiles(Item, DefaultMask));
613 | }
614 | else if (File.Exists(Item))
615 | {
616 | Result.Add(Item);
617 | }
618 | else
619 | {
620 | Result.AddRange(FindFiles(Path.GetDirectoryName(Item), Path.GetFileName(Item)));
621 | }
622 | }
623 | return Result;
624 | }
625 |
626 | static List FindFiles(string Dir, string Mask)
627 | {
628 | List Result = new List();
629 | foreach (string Item in Directory.EnumerateFiles(Dir.Length > 0 ? Dir : ".", Mask, SearchOption.TopDirectoryOnly))
630 | {
631 | Result.Add(Item);
632 | }
633 | return Result;
634 | }
635 |
636 | static bool ParseSwitch(List ArgsList, string Name)
637 | {
638 | for (int Idx = 0; Idx < ArgsList.Count; Idx++)
639 | {
640 | if (String.Compare(ArgsList[Idx], Name, true) == 0)
641 | {
642 | ArgsList.RemoveAt(Idx);
643 | return true;
644 | }
645 | }
646 | return false;
647 | }
648 |
649 | static string ParseParameter(List ArgsList, string Prefix, string Default)
650 | {
651 | string Value = Default;
652 | for (int Idx = 0; Idx < ArgsList.Count; Idx++)
653 | {
654 | if (ArgsList[Idx].StartsWith(Prefix, StringComparison.CurrentCultureIgnoreCase))
655 | {
656 | Value = ArgsList[Idx].Substring(Prefix.Length);
657 | ArgsList.RemoveAt(Idx);
658 | break;
659 | }
660 | }
661 | return Value;
662 | }
663 |
664 | static IEnumerable ParseParameters(List ArgsList, string Prefix)
665 | {
666 | for (; ; )
667 | {
668 | string Value = ParseParameter(ArgsList, Prefix, null);
669 | if (Value == null)
670 | {
671 | break;
672 | }
673 | yield return Value;
674 | }
675 | }
676 | }
677 | }
678 |
--------------------------------------------------------------------------------
/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | // General Information about an assembly is controlled through the following
6 | // set of attributes. Change these attribute values to modify the information
7 | // associated with an assembly.
8 | [assembly: AssemblyTitle("GitDepsPacker")]
9 | [assembly: AssemblyDescription("")]
10 | [assembly: AssemblyConfiguration("")]
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # GitDepsPacker utility
2 |
3 | [](https://github.com/bozaro/UE4GitDepsPacker/blob/master/LICENSE)
4 | [](https://travis-ci.org/bozaro/UE4GitDepsPacker)
5 |
6 |
7 | This utility allows to create ```*.gitdeps.xml``` files for Unreal Engine 4.7+.
8 |
9 | ## How to use
10 |
11 | Building:
12 |
13 | * Copy this repository (or link as git submodule) to ```Engine/Source/Programs/GitDepsPacker/```.
14 | * Build Unreal Engine.
15 |
16 | ## Generating ```*.gitdeps.xml``` files
17 |
18 | You can generate ```*.gitdeps.xml``` files by running command like:
19 |
20 | ```
21 | Engine\Binaries\DotNET\GitDepsPacker.exe Engine/Content/SomeTool Engine/Source/ThirdParty/SomeTool !**/*.pdb --base-url=http://cdn.local/gitdeps/sometool/ --ignore-proxy --remote-path=1.0 --target=Engine/Build/SomeTool.gitdeps.xml --storage=\\cdn\share\sometool\1.0 --ignore-git
22 | ```
23 |
24 | This command:
25 |
26 | * Create UEPACK-files and place them to network storage: ```\\cdn\share\sometool\1.0```;
27 | * Generage Engine/Build/SomeTool.gitdeps.xml file;
28 | * Remove all packed files from old ```Engine/Build/*.gitdeps.xml``` files.
29 |
30 | Pack files will contains:
31 |
32 | * Files in directory ```Engine/Source/ThirdParty/SomeTool```;
33 | * Files in directory ```Engine/Content/SomeTool```;
34 | * Excluding ```*.pdb``` files in all directories;
35 | * Unchanged files already contains in other ```Engine/Build/*.gitdeps.xml``` files will be excluded;
36 | * Files stored in git will be excluded (--ignore-git flag).
37 |
--------------------------------------------------------------------------------
/StatHelper.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Reflection;
4 |
5 | namespace GitDepsPacker
6 | {
7 | static class StatFileHelper
8 | {
9 | const uint ExecutableBits = (1 << 0) | (1 << 3) | (1 << 6);
10 |
11 | private static MethodInfo StatMethod;
12 | private static FieldInfo StatModeField;
13 |
14 | static StatFileHelper()
15 | {
16 | StatMethod = null;
17 |
18 | // Try to load the Mono Posix assembly. If it doesn't exist, we're on Windows.
19 | Assembly MonoPosix;
20 | try
21 | {
22 | MonoPosix = Assembly.Load("Mono.Posix, Version=4.0.0.0, Culture=neutral, PublicKeyToken=0738eb9f132ed756");
23 | }
24 | catch (FileNotFoundException)
25 | {
26 | return;
27 | }
28 | Type SyscallType = MonoPosix.GetType("Mono.Unix.Native.Syscall");
29 | if(SyscallType == null)
30 | {
31 | throw new InvalidOperationException("Couldn't find Syscall type");
32 | }
33 | StatMethod = SyscallType.GetMethod ("stat");
34 | if (StatMethod == null)
35 | {
36 | throw new InvalidOperationException("Couldn't find Mono.Unix.Native.Syscall.stat method");
37 | }
38 | Type StatType = MonoPosix.GetType("Mono.Unix.Native.Stat");
39 | if(StatType == null)
40 | {
41 | throw new InvalidOperationException("Couldn't find Mono.Unix.Native.Stat type");
42 | }
43 | StatModeField = StatType.GetField("st_mode");
44 | if(StatModeField == null)
45 | {
46 | throw new InvalidOperationException("Couldn't find Mono.Unix.Native.Stat.st_mode field");
47 | }
48 | }
49 |
50 | public static bool IsExecutalbe(string FileName)
51 | {
52 | if (StatMethod == null) {
53 | return false;
54 | }
55 |
56 | object[] StatArgs = new object[] { FileName, null };
57 | int StatResult = (int)StatMethod.Invoke(null, StatArgs);
58 | if (StatResult != 0)
59 | {
60 | throw new InvalidOperationException(String.Format("Stat() call for {0} failed with error {1}", FileName, StatResult));
61 | }
62 | // Get the current permissions
63 | uint CurrentPermissions = (uint)StatModeField.GetValue(StatArgs[1]);
64 | return (CurrentPermissions & ExecutableBits) != 0;
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/Wildcard.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Text.RegularExpressions;
6 |
7 | namespace GitDepsPacker
8 | {
9 | enum EMatch
10 | {
11 | MatchEquals,
12 | MatchRegex,
13 | MatchSubtree
14 | }
15 |
16 | struct WildcardItem
17 | {
18 | public EMatch Match;
19 | public string Mask;
20 |
21 | public WildcardItem(EMatch Match, string Mask)
22 | {
23 | this.Match = Match;
24 | this.Mask = Mask;
25 | }
26 | }
27 |
28 | class Wildcard
29 | {
30 | private static char[] Separators = { '/', '\\' };
31 | private WildcardItem[] Parts;
32 | private bool ExcludeMask;
33 |
34 | public Wildcard(string Mask)
35 | {
36 | ExcludeMask = Mask.StartsWith("!");
37 | Parts = ParseWildcard(Mask.Substring(ExcludeMask ? 1 : 0));
38 | }
39 |
40 | private static WildcardItem[] ParseWildcard(string Mask)
41 | {
42 | string[] Parts = Mask.Split(Separators, StringSplitOptions.RemoveEmptyEntries);
43 | WildcardItem[] Result = new WildcardItem[Parts.Length];
44 | for (int i = 0; i < Parts.Length; ++i)
45 | {
46 | string Part = Parts[i];
47 | if (Part == "**")
48 | {
49 | Result[i] = new WildcardItem(EMatch.MatchSubtree, null);
50 | }
51 | else
52 | {
53 | string Escaped = Regex.Escape(Part);
54 | string Pattern = Escaped.Replace("\\*", ".*").Replace("\\?", ".");
55 | if (Pattern != Escaped)
56 | {
57 | Result[i] = new WildcardItem(EMatch.MatchRegex, Pattern);
58 | }
59 | else
60 | {
61 | Result[i] = new WildcardItem(EMatch.MatchEquals, Part);
62 | }
63 | }
64 | }
65 | return Result;
66 | }
67 |
68 | public bool Exclude
69 | {
70 | get
71 | {
72 | return ExcludeMask;
73 | }
74 | }
75 |
76 | public bool IsMatched(string path, bool FilePath)
77 | {
78 | return CheckMatched(path.Split(Separators, StringSplitOptions.RemoveEmptyEntries), 0, 0, FilePath);
79 | }
80 |
81 | private bool CheckMatched(string[] Items, int ItemsPos, int PartsPos, bool FilePath)
82 | {
83 | if (PartsPos >= Parts.Length)
84 | {
85 | return true;
86 | }
87 | if (ItemsPos >= Items.Length)
88 | {
89 | return !(FilePath || ExcludeMask);
90 | }
91 | switch (Parts[PartsPos].Match)
92 | {
93 | case EMatch.MatchEquals:
94 | if (!Parts[PartsPos].Mask.Equals(Items[ItemsPos], StringComparison.InvariantCultureIgnoreCase))
95 | {
96 | return false;
97 | }
98 | break;
99 | case EMatch.MatchRegex:
100 | if (!Regex.IsMatch(Items[ItemsPos], Parts[PartsPos].Mask, RegexOptions.IgnoreCase))
101 | {
102 | return false;
103 | }
104 | break;
105 | case EMatch.MatchSubtree:
106 | if (CheckMatched(Items, ItemsPos + 1, PartsPos, FilePath))
107 | {
108 | return true;
109 | }
110 | break;
111 | }
112 | return CheckMatched(Items, ItemsPos + 1, PartsPos + 1, FilePath);
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/WriteStream.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using System.Text;
6 |
7 | namespace GitDepsPacker
8 | {
9 | class WriteStream : Stream
10 | {
11 | Stream Inner;
12 | long Pos;
13 |
14 | public WriteStream(Stream Inner)
15 | {
16 | this.Inner = Inner;
17 | this.Pos = 0;
18 | }
19 |
20 | protected override void Dispose(bool Disposing)
21 | {
22 | if (Inner != null)
23 | {
24 | Inner.Dispose();
25 | Inner = null;
26 | }
27 | }
28 |
29 | public override bool CanRead
30 | {
31 | get { return false; }
32 | }
33 |
34 | public override bool CanWrite
35 | {
36 | get { return true; }
37 | }
38 |
39 | public override bool CanSeek
40 | {
41 | get { return false; }
42 | }
43 |
44 | public override long Position
45 | {
46 | get
47 | {
48 | return Pos;
49 | }
50 | set
51 | {
52 | throw new NotImplementedException();
53 | }
54 | }
55 |
56 | public override long Length
57 | {
58 | get { throw new NotImplementedException(); }
59 | }
60 |
61 | public override void SetLength(long Value)
62 | {
63 | throw new NotImplementedException();
64 | }
65 |
66 | public override int Read(byte[] Buffer, int Offset, int Count)
67 | {
68 | throw new NotImplementedException();
69 | }
70 |
71 | public override void Write(byte[] Buffer, int Offset, int Count)
72 | {
73 | Inner.Write(Buffer, Offset, Count);
74 | Pos += Count;
75 | }
76 |
77 | public override long Seek(long Offset, SeekOrigin Origin)
78 | {
79 | throw new NotImplementedException();
80 | }
81 |
82 | public override void Flush()
83 | {
84 | Inner.Flush();
85 | }
86 | }
87 | }
88 |
--------------------------------------------------------------------------------