markus / MarkusAutoUpdate / src / NetSparkle.Tools.AppCastGenerator / Program.cs @ 77cdac33
이력 | 보기 | 이력해설 | 다운로드 (15.7 KB)
1 | d8f5045e | taeseongkim | using NetSparkleUpdater; |
---|---|---|---|
2 | using System; |
||
3 | using System.Diagnostics; |
||
4 | using System.IO; |
||
5 | using System.Collections.Generic; |
||
6 | using System.Xml; |
||
7 | using NetSparkleUpdater.AppCastHandlers; |
||
8 | using System.Text; |
||
9 | using CommandLine; |
||
10 | using System.Text.RegularExpressions; |
||
11 | using System.Linq; |
||
12 | using Console = Colorful.Console; |
||
13 | using System.Drawing; |
||
14 | using NetSparkleUpdater.AppCastGenerator; |
||
15 | using System.Web; |
||
16 | using System.Net.Http.Headers; |
||
17 | using System.Threading; |
||
18 | using System.Net; |
||
19 | using System.IO.Compression; |
||
20 | |||
21 | namespace NetSparkleUpdater.Tools.AppCastGenerator |
||
22 | { |
||
23 | internal class Program |
||
24 | { |
||
25 | public class Options |
||
26 | { |
||
27 | [Option('a', "appcast-output-directory", Required = false, HelpText = "Directory to write appcast.xml")] |
||
28 | public string OutputDirectory { get; set; } |
||
29 | |||
30 | [Option('e', "ext", SetName = "local", Required = false, HelpText = "Search for file extensions.", Default = "exe")] |
||
31 | public string Extension { get; set; } |
||
32 | |||
33 | [Option('b', "binaries", SetName = "local", Required = false, HelpText = "Directory containing binaries.", Default = ".")] |
||
34 | public string SourceBinaryDirectory { get; set; } |
||
35 | |||
36 | //[Option('g', "github-atom-feed", SetName = "github", Required = false, HelpText = "Generate from Github release atom feed (signatures not supported yet)")] |
||
37 | //public string GithubAtomFeed { get; set; } |
||
38 | |||
39 | [Option('f', "file-extract-version", SetName = "local", Required = false, HelpText = "Determine the version from the file name", Default = false)] |
||
40 | public bool FileExtractVersion { get; set; } |
||
41 | |||
42 | [Option('o', "os", Required = false, HelpText = "Operating System (windows,macos,linux)", Default = "windows")] |
||
43 | public string OperatingSystem { get; set; } |
||
44 | |||
45 | [Option('u', "base-url", SetName = "local", Required = false, HelpText = "Base URL for downloads", Default = null)] |
||
46 | public Uri BaseUrl { get; set; } |
||
47 | |||
48 | [Option('l', "change-log-url", SetName = "local", Required = false, HelpText = "Base URL to the location for your changelog files on some server for downloading", Default = "")] |
||
49 | public string ChangeLogUrl { get; set; } |
||
50 | |||
51 | [Option('p', "change-log-path", SetName = "local", Required = false, HelpText = "File path to Markdown changelog files (expected extension: .md; version must match AssemblyVersion, e.g. MyApp 1.0.0.md).", Default = "")] |
||
52 | public string ChangeLogPath { get; set; } |
||
53 | |||
54 | [Option('n', "product-name", Required = false, HelpText = "Product Name", Default = "Application")] |
||
55 | public string ProductName { get; set; } |
||
56 | |||
57 | [Option('x', "url-prefix-version", SetName = "local", Required = false, HelpText = "Add the version as a prefix to the download url")] |
||
58 | public bool PrefixVersion { get; set; } |
||
59 | |||
60 | [Option('v', "Fixed version", SetName = "local", Required = false, HelpText = "Fixed Version (All files) Option",Default =null)] |
||
61 | public string FixedVersion { get; set; } |
||
62 | |||
63 | [Option("key-path", SetName = "local", Required = false, HelpText = "Path to NetSparkle_Ed25519.priv and NetSparkle_Ed25519.pub files")] |
||
64 | public string PathToKeyFiles { get; set; } |
||
65 | |||
66 | |||
67 | #region Key Generation |
||
68 | |||
69 | 77cdac33 | taeseongkim | [Option("generate-keys", SetName = "local", Required = false, HelpText = "Generate keys")] |
70 | d8f5045e | taeseongkim | public bool GenerateKeys { get; set; } |
71 | |||
72 | [Option("force", SetName = "keys", Required = false, HelpText = "Force regeneration of keys")] |
||
73 | public bool ForceRegeneration { get; set; } |
||
74 | |||
75 | [Option("export", SetName = "keys", Required = false, HelpText = "Export keys")] |
||
76 | public bool Export { get; set; } |
||
77 | |||
78 | #endregion |
||
79 | |||
80 | |||
81 | #region Getting Signatures for Binaries |
||
82 | |||
83 | [Option("generate-signature", SetName = "signing", Required = false, HelpText = "Generate signature from binary")] |
||
84 | public string BinaryToSign { get; set; } |
||
85 | |||
86 | #endregion |
||
87 | |||
88 | #region Verifying Binary Signatures |
||
89 | |||
90 | [Option("verify", SetName = "verify", Required = false, HelpText = "Binary to verify")] |
||
91 | public string BinaryToVerify { get; set; } |
||
92 | |||
93 | [Option("signature", SetName = "verify", Required = false, HelpText = "Signature")] |
||
94 | public string Signature { get; set; } |
||
95 | |||
96 | #endregion |
||
97 | |||
98 | } |
||
99 | |||
100 | |||
101 | private static string[] _operatingSystems = new string[] { "windows", "mac", "linux" }; |
||
102 | private static SignatureManager _signatureManager = new SignatureManager(); |
||
103 | |||
104 | static void Main(string[] args) |
||
105 | { |
||
106 | Parser.Default.ParseArguments<Options>(args) |
||
107 | .WithParsed(Run) |
||
108 | .WithNotParsed(HandleParseError); |
||
109 | } |
||
110 | static void Run(Options opts) |
||
111 | { |
||
112 | /* |
||
113 | if (opts.GithubAtomFeed != null) |
||
114 | { |
||
115 | GenerateFromAtom(opts); |
||
116 | return; |
||
117 | }*/ |
||
118 | |||
119 | if (!string.IsNullOrWhiteSpace(opts.PathToKeyFiles)) |
||
120 | { |
||
121 | _signatureManager.SetStorageDirectory(opts.PathToKeyFiles); |
||
122 | } |
||
123 | |||
124 | if (opts.Export) |
||
125 | { |
||
126 | Console.WriteLine("Private Key:"); |
||
127 | Console.WriteLine(Convert.ToBase64String(_signatureManager.GetPrivateKey())); |
||
128 | Console.WriteLine("Public Key:"); |
||
129 | Console.WriteLine(Convert.ToBase64String(_signatureManager.GetPublicKey())); |
||
130 | return; |
||
131 | } |
||
132 | |||
133 | if (opts.GenerateKeys) |
||
134 | { |
||
135 | var didSucceed = _signatureManager.Generate(opts.ForceRegeneration); |
||
136 | if (didSucceed) |
||
137 | { |
||
138 | Console.WriteLine("Keys successfully generated", Color.Green); |
||
139 | } |
||
140 | else |
||
141 | { |
||
142 | Console.WriteLine("Keys failed to generate", Color.Red); |
||
143 | } |
||
144 | return; |
||
145 | } |
||
146 | |||
147 | if (opts.BinaryToSign != null) |
||
148 | { |
||
149 | var signature = _signatureManager.GetSignatureForFile(new FileInfo(opts.BinaryToSign)); |
||
150 | |||
151 | Console.WriteLine($"Signature: {signature}", Color.Green); |
||
152 | |||
153 | return; |
||
154 | } |
||
155 | |||
156 | if (opts.BinaryToVerify != null) |
||
157 | { |
||
158 | var result = _signatureManager.VerifySignature(new FileInfo(opts.BinaryToVerify), opts.Signature); |
||
159 | |||
160 | if (result) |
||
161 | { |
||
162 | Console.WriteLine($"Signature valid", Color.Green); |
||
163 | } |
||
164 | else |
||
165 | { |
||
166 | Console.WriteLine($"Signature invalid", Color.Red); |
||
167 | } |
||
168 | |||
169 | return; |
||
170 | } |
||
171 | |||
172 | |||
173 | var search = $"*.{opts.Extension}"; |
||
174 | |||
175 | if (opts.SourceBinaryDirectory == ".") |
||
176 | { |
||
177 | opts.SourceBinaryDirectory = Environment.CurrentDirectory; |
||
178 | } |
||
179 | |||
180 | var binaries = Directory.GetFiles(opts.SourceBinaryDirectory, search,SearchOption.AllDirectories); |
||
181 | |||
182 | if (binaries.Length == 0) |
||
183 | { |
||
184 | Console.WriteLine($"No files founds matching {search} in {opts.SourceBinaryDirectory}", Color.Yellow); |
||
185 | Environment.Exit(1); |
||
186 | } |
||
187 | |||
188 | if (!_operatingSystems.Any(opts.OperatingSystem.Contains)) |
||
189 | { |
||
190 | Console.WriteLine($"Invalid operating system: {opts.OperatingSystem}", Color.Red); |
||
191 | Console.WriteLine($"Valid options are : windows, macos or linux"); |
||
192 | Environment.Exit(1); |
||
193 | } |
||
194 | |||
195 | if (string.IsNullOrEmpty(opts.OutputDirectory)) |
||
196 | { |
||
197 | opts.OutputDirectory = opts.SourceBinaryDirectory; |
||
198 | } |
||
199 | |||
200 | Console.WriteLine(""); |
||
201 | Console.WriteLine($"Operating System: {opts.OperatingSystem}", Color.Blue); |
||
202 | Console.WriteLine($"Searching: {opts.SourceBinaryDirectory}", Color.Blue); |
||
203 | Console.WriteLine($"Found {binaries.Count()} {opts.Extension} files(s)", Color.Blue); |
||
204 | Console.WriteLine(""); |
||
205 | |||
206 | try |
||
207 | { |
||
208 | |||
209 | var productName = opts.ProductName; |
||
210 | |||
211 | var items = new List<AppCastItem>(); |
||
212 | |||
213 | var usesChangelogs = !string.IsNullOrWhiteSpace(opts.ChangeLogPath) && Directory.Exists(opts.ChangeLogPath); |
||
214 | |||
215 | if(string.IsNullOrWhiteSpace(opts.FixedVersion)) |
||
216 | { |
||
217 | Console.WriteLine("FixedVersion Null. setting -v x.x.x", Color.Red); |
||
218 | Environment.Exit(1); |
||
219 | } |
||
220 | |||
221 | string tempfile = System.IO.Path.Combine(System.IO.Path.GetTempPath(), System.IO.Path.GetRandomFileName()); |
||
222 | |||
223 | CreateZip(opts.SourceBinaryDirectory, tempfile); |
||
224 | |||
225 | var fileInfo = new FileInfo(tempfile); |
||
226 | |||
227 | var fileVersion = FileVersion(fileInfo,opts.FixedVersion,opts.FileExtractVersion); |
||
228 | |||
229 | var productVersion = fileVersion; |
||
230 | var itemTitle = string.IsNullOrWhiteSpace(productName) ? productVersion : productName + " " + productVersion; |
||
231 | |||
232 | var remoteUpdateFileName = $"{productName}_{opts.FixedVersion}.zip"; |
||
233 | |||
234 | var remoteUpdateFilePath = $"{(opts.PrefixVersion ? $"{fileVersion}/" :"")}{HttpUtility.UrlEncode(remoteUpdateFileName)}"; |
||
235 | |||
236 | Copy(tempfile, Path.Combine(opts.OutputDirectory, remoteUpdateFilePath.Replace('/', '\\'))); |
||
237 | |||
238 | var remoteUpdateFile = $"{opts.BaseUrl}{remoteUpdateFilePath}"; |
||
239 | |||
240 | // changelog stuff |
||
241 | var changelogFileName = productVersion + ".md"; |
||
242 | var changelogPath = Path.Combine(opts.ChangeLogPath, changelogFileName); |
||
243 | var hasChangelogForFile = usesChangelogs && File.Exists(changelogPath); |
||
244 | var changelogSignature = ""; |
||
245 | |||
246 | if (hasChangelogForFile) |
||
247 | { |
||
248 | changelogSignature = _signatureManager.GetSignatureForFile(changelogPath); |
||
249 | } |
||
250 | |||
251 | // |
||
252 | var item = new AppCastItem() |
||
253 | { |
||
254 | Title = itemTitle, |
||
255 | DownloadLink = remoteUpdateFile, |
||
256 | Version = productVersion, |
||
257 | ShortVersion = productVersion.Substring(0, productVersion.LastIndexOf('.')), |
||
258 | PublicationDate = fileInfo.CreationTime, |
||
259 | UpdateSize = fileInfo.Length, |
||
260 | Description = "", |
||
261 | DownloadSignature = _signatureManager.KeysExist() ? _signatureManager.GetSignatureForFile(fileInfo) : null, |
||
262 | OperatingSystemString = opts.OperatingSystem, |
||
263 | MIMEType = MimeTypes.GetMimeType(fileInfo.Name) |
||
264 | }; |
||
265 | |||
266 | if (hasChangelogForFile) |
||
267 | { |
||
268 | if (!string.IsNullOrWhiteSpace(opts.ChangeLogUrl)) |
||
269 | { |
||
270 | item.ReleaseNotesSignature = changelogSignature; |
||
271 | item.ReleaseNotesLink = opts.ChangeLogUrl + changelogFileName; |
||
272 | } |
||
273 | else |
||
274 | { |
||
275 | item.Description = File.ReadAllText(changelogPath); |
||
276 | } |
||
277 | } |
||
278 | |||
279 | items.Add(item); |
||
280 | |||
281 | // appcast 생성 폴더에 업데이트가 필요한 파일을 복사 |
||
282 | |||
283 | |||
284 | |||
285 | var appcastXmlDocument = XMLAppCast.GenerateAppCastXml(items, productName); |
||
286 | |||
287 | var appcastFileName = Path.Combine(opts.OutputDirectory, "appcast.xml"); |
||
288 | |||
289 | var dirName = Path.GetDirectoryName(appcastFileName); |
||
290 | |||
291 | if (!Directory.Exists(dirName)) |
||
292 | { |
||
293 | Console.WriteLine("Creating {0}", dirName); |
||
294 | Directory.CreateDirectory(dirName); |
||
295 | } |
||
296 | else |
||
297 | { |
||
298 | if(File.Exists(appcastFileName)) |
||
299 | { |
||
300 | File.Copy(appcastFileName, $"{ appcastFileName}{ opts.FixedVersion}", true); |
||
301 | } |
||
302 | } |
||
303 | |||
304 | Console.WriteLine("Writing appcast to {0}", appcastFileName); |
||
305 | |||
306 | using (var w = XmlWriter.Create(appcastFileName, new XmlWriterSettings { NewLineChars = "\n", Encoding = new UTF8Encoding(false) })) |
||
307 | { |
||
308 | appcastXmlDocument.Save(w); |
||
309 | } |
||
310 | |||
311 | if (_signatureManager.KeysExist()) |
||
312 | { |
||
313 | var appcastFile = new FileInfo(appcastFileName); |
||
314 | var signatureFile = appcastFileName + ".signature"; |
||
315 | |||
316 | if (File.Exists(signatureFile)) |
||
317 | { |
||
318 | File.Copy(signatureFile, $"{ signatureFile}{ opts.FixedVersion}",true); |
||
319 | } |
||
320 | |||
321 | var signature = _signatureManager.GetSignatureForFile(appcastFile); |
||
322 | |||
323 | var result = _signatureManager.VerifySignature(appcastFile, signature); |
||
324 | |||
325 | if (result) |
||
326 | { |
||
327 | File.WriteAllText(signatureFile, signature); |
||
328 | Console.WriteLine($"Wrote {signatureFile}", Color.Green); |
||
329 | } |
||
330 | else |
||
331 | { |
||
332 | Console.WriteLine($"Failed to verify {signatureFile}", Color.Red); |
||
333 | } |
||
334 | } |
||
335 | else |
||
336 | { |
||
337 | Console.WriteLine("Skipped generating signature. Generate keys with --generate-keys", Color.Red); |
||
338 | Environment.Exit(1); |
||
339 | } |
||
340 | } |
||
341 | catch (Exception e) |
||
342 | { |
||
343 | Console.WriteLine(e.Message); |
||
344 | Console.WriteLine(); |
||
345 | Environment.Exit(1); |
||
346 | } |
||
347 | } |
||
348 | |||
349 | private static string[] CreateZip(string sourceBinaryDirectory,string targetFile) |
||
350 | { |
||
351 | ZipFile.CreateFromDirectory(sourceBinaryDirectory, targetFile); |
||
352 | |||
353 | return new[] { targetFile }; |
||
354 | } |
||
355 | |||
356 | private static string FileVersion(FileInfo fileInfo,string fixedVersion,bool fileExtractVersion) |
||
357 | { |
||
358 | string version = ""; |
||
359 | |||
360 | if (!string.IsNullOrWhiteSpace(fixedVersion)) |
||
361 | { |
||
362 | version = fixedVersion; |
||
363 | } |
||
364 | else |
||
365 | { |
||
366 | if (fileExtractVersion) |
||
367 | { |
||
368 | version = GetVersionFromName(fileInfo); |
||
369 | } |
||
370 | else |
||
371 | { |
||
372 | version = GetVersionFromAssembly(fileInfo); |
||
373 | |||
374 | } |
||
375 | } |
||
376 | |||
377 | return version; |
||
378 | } |
||
379 | |||
380 | |||
381 | static private void Copy(string sourceFileName, string destFileName) |
||
382 | { |
||
383 | try |
||
384 | { |
||
385 | var dirName = Path.GetDirectoryName(destFileName); |
||
386 | |||
387 | if (!Directory.Exists(dirName)) |
||
388 | { |
||
389 | Console.WriteLine("Creating {0}", dirName); |
||
390 | Directory.CreateDirectory(dirName); |
||
391 | } |
||
392 | |||
393 | File.Copy(sourceFileName, destFileName,true); |
||
394 | } |
||
395 | catch (Exception e) |
||
396 | { |
||
397 | Console.WriteLine(e.Message); |
||
398 | Console.WriteLine(); |
||
399 | Environment.Exit(1); |
||
400 | } |
||
401 | } |
||
402 | |||
403 | static void HandleParseError(IEnumerable<Error> errs) |
||
404 | { |
||
405 | errs.Output(); |
||
406 | } |
||
407 | |||
408 | static string GetVersionFromName(FileInfo fileInfo) |
||
409 | { |
||
410 | var regexPattern = @"\d+(\.\d+)+"; |
||
411 | var regex = new Regex(regexPattern); |
||
412 | |||
413 | var match = regex.Match(fileInfo.FullName); |
||
414 | |||
415 | return match.Captures[match.Captures.Count - 1].Value; // get the numbers at the end of the string incase the app is something like 1.0application1.0.0.dmg |
||
416 | } |
||
417 | |||
418 | static string GetVersionFromAssembly(FileInfo fileInfo) |
||
419 | { |
||
420 | return FileVersionInfo.GetVersionInfo(fileInfo.FullName).ProductVersion; |
||
421 | } |
||
422 | |||
423 | |||
424 | } |
||
425 | } |