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