markus / MarkusAutoUpdate / src / NetSparkle.Tools.AppCastGenerator / Program.cs @ 7e810f54
이력 | 보기 | 이력해설 | 다운로드 (15.7 KB)
1 |
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 |
[Option("generate-keys", SetName = "local", Required = false, HelpText = "Generate keys")] |
70 |
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 |
} |