mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
First draft of update icon replacement feature
This commit is contained in:
@@ -21,7 +21,7 @@ namespace SquirrelCli
|
||||
public string package { get; set; }
|
||||
public string splashImage { get; private set; }
|
||||
public string setupIcon { get; private set; }
|
||||
//public string appIcon { get; private set; }
|
||||
public string updateIcon { get; private set; }
|
||||
public string signParams { get; private set; }
|
||||
public string framework { get; private set; }
|
||||
public bool noDelta { get; private set; }
|
||||
@@ -30,19 +30,20 @@ namespace SquirrelCli
|
||||
public ReleasifyOptions()
|
||||
{
|
||||
Add("p=|package=", "Path to a nuget package to releasify", v => package = v);
|
||||
Add("b=|baseUrl=", "Provides a base URL to prefix the RELEASES file packages with", v => baseUrl = v, true);
|
||||
Add("s=|splashImage=", "Image to be displayed during installation (can be jpg, png, gif, etc)", v => splashImage = v);
|
||||
//Add("i=|appIcon=", "ICO file that will be used for app shortcuts", v => appIcon = v);
|
||||
Add("setupIcon=", "ICO file that will be used for Setup.exe", v => setupIcon = v);
|
||||
Add("n=|signParams=", "Sign the installer via SignTool.exe with the parameters given", v => signParams = v);
|
||||
Add("f=|framework=", "Set the required .NET framework version, e.g. net461", v => framework = v);
|
||||
Add("setupIcon=", "ICO file that will be used for Setup.exe", v => setupIcon = v);
|
||||
Add("updateIcon=", "ICO file that will be used for Update.exe", v => updateIcon = v);
|
||||
Add("no-delta", "Don't generate delta packages to save time", v => noDelta = true);
|
||||
Add("b=|baseUrl=", "Provides a base URL to prefix the RELEASES file packages with", v => baseUrl = v, true);
|
||||
Add("addSearchPath=", "Add additional search directories when looking for helper exe's such as Setup.exe, Update.exe, etc", v => HelperExe.AddSearchPath(v));
|
||||
}
|
||||
|
||||
public override void Validate()
|
||||
{
|
||||
IsValidFile(nameof(setupIcon), ".ico");
|
||||
IsValidFile(nameof(updateIcon), ".ico");
|
||||
IsValidFile(nameof(splashImage));
|
||||
IsValidUrl(nameof(baseUrl));
|
||||
IsRequired(nameof(package));
|
||||
|
||||
@@ -123,13 +123,19 @@ namespace SquirrelCli
|
||||
var backgroundGif = options.splashImage;
|
||||
var setupIcon = options.setupIcon;
|
||||
|
||||
var updatePath = HelperExe.UpdatePath;
|
||||
|
||||
// validate that the provided "frameworkVersion" is supported by Setup.exe
|
||||
if (!String.IsNullOrWhiteSpace(frameworkVersion)) {
|
||||
HelperExe.ValidateFrameworkVersion(frameworkVersion).Wait();
|
||||
}
|
||||
|
||||
// update icon for Update.exe if requested
|
||||
var updatePath = HelperExe.UpdatePath;
|
||||
using var ud = Utility.WithTempDirectory(out var updateDir);
|
||||
if (options.updateIcon != null) {
|
||||
updatePath = Path.Combine(updateDir, "Update.exe");
|
||||
SingleFileBundle.UpdateSingleFileIcon(HelperExe.UpdatePath, updatePath, options.updateIcon).Wait();
|
||||
}
|
||||
|
||||
// copy input package to target output directory
|
||||
if (!package.EndsWith(".nupkg", StringComparison.InvariantCultureIgnoreCase))
|
||||
throw new ArgumentException("package must be packed with nuget and end in '.nupkg'");
|
||||
|
||||
230
src/SquirrelCli/SingleFileBundle.cs
Normal file
230
src/SquirrelCli/SingleFileBundle.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
// Parts of this file have been used from
|
||||
// https://github.com/icsharpcode/ILSpy/blob/f7460a041ea8fb8b0abf8527b97a5b890eb94eea/ICSharpCode.Decompiler/SingleFileBundle.cs
|
||||
|
||||
using Microsoft.NET.HostModel.AppHost;
|
||||
using Microsoft.NET.HostModel.Bundle;
|
||||
using Squirrel;
|
||||
using Squirrel.SimpleSplat;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.IO.MemoryMappedFiles;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SquirrelCli
|
||||
{
|
||||
internal class SingleFileBundle
|
||||
{
|
||||
private static IFullLogger Log = SquirrelLocator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(SingleFileBundle));
|
||||
|
||||
public static async Task UpdateSingleFileIcon(string sourceFile, string destinationFile, string iconPath)
|
||||
{
|
||||
using var d = Utility.WithTempDirectory(out var tmpdir);
|
||||
var hostPath = Path.Combine(tmpdir, "singlefilehost.exe");
|
||||
var sourceName = Path.GetFileNameWithoutExtension(sourceFile);
|
||||
|
||||
// extract bundled host to file
|
||||
using (var hostStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("SquirrelCli.singlefilehost.exe"))
|
||||
using (var file = new FileStream(hostPath, FileMode.Create, FileAccess.Write)) {
|
||||
hostStream.CopyTo(file);
|
||||
}
|
||||
|
||||
// extract Update.exe to tmp dir
|
||||
Log.Info("Extracting Update.exe resources to temp directory");
|
||||
DumpPackageAssemblies(sourceFile, tmpdir);
|
||||
|
||||
// create new app host
|
||||
var newAppHost = Path.Combine(tmpdir, sourceName + ".exe");
|
||||
HostWriter.CreateAppHost(
|
||||
hostPath, // input file
|
||||
newAppHost, // output file
|
||||
sourceName + ".dll", // entry point, relative to apphost
|
||||
true, // isGui?
|
||||
Path.Combine(tmpdir, sourceName + ".dll") // copy exe resources from?
|
||||
);
|
||||
File.Delete(hostPath);
|
||||
|
||||
// set new icon
|
||||
Log.Info("Patching Update.exe icon");
|
||||
await HelperExe.SetExeIcon(newAppHost, iconPath);
|
||||
|
||||
// create new bundle
|
||||
var bundlerOutput = Path.Combine(tmpdir, "output");
|
||||
Directory.CreateDirectory(bundlerOutput);
|
||||
var bundler = new Bundler(
|
||||
sourceName + ".exe",
|
||||
bundlerOutput,
|
||||
BundleOptions.EnableCompression,
|
||||
OSPlatform.Windows,
|
||||
Architecture.X86,
|
||||
new Version(6, 0),
|
||||
false,
|
||||
sourceName
|
||||
);
|
||||
|
||||
Log.Info("Re-packing Update.exe bundle");
|
||||
var singleFile = SingleFileBundle.GenerateBundle(bundler, tmpdir, bundlerOutput);
|
||||
|
||||
// copy to requested location
|
||||
File.Copy(singleFile, destinationFile);
|
||||
}
|
||||
|
||||
private static void DumpPackageAssemblies(string packageFileName, string outputDirectory)
|
||||
{
|
||||
if (!HostWriter.IsBundle(packageFileName, out long bundleHeaderOffset)) {
|
||||
throw new InvalidOperationException($"Cannot dump assembiles for {packageFileName}, because it is not a single file bundle.");
|
||||
}
|
||||
|
||||
using (var memoryMappedPackage = MemoryMappedFile.CreateFromFile(packageFileName, FileMode.Open, null, 0, MemoryMappedFileAccess.Read)) {
|
||||
using (var packageView = memoryMappedPackage.CreateViewAccessor(0, 0, MemoryMappedFileAccess.Read)) {
|
||||
var manifest = SingleFileBundle.ReadManifest(packageView, bundleHeaderOffset);
|
||||
foreach (var entry in manifest.Entries) {
|
||||
Stream contents;
|
||||
|
||||
if (entry.CompressedSize == 0) {
|
||||
contents = new UnmanagedMemoryStream(packageView.SafeMemoryMappedViewHandle, entry.Offset, entry.Size);
|
||||
} else {
|
||||
Stream compressedStream = new UnmanagedMemoryStream(packageView.SafeMemoryMappedViewHandle, entry.Offset, entry.CompressedSize);
|
||||
Stream decompressedStream = new MemoryStream((int) entry.Size);
|
||||
using (var deflateStream = new DeflateStream(compressedStream, CompressionMode.Decompress)) {
|
||||
deflateStream.CopyTo(decompressedStream);
|
||||
}
|
||||
|
||||
if (decompressedStream.Length != entry.Size) {
|
||||
throw new Exception($"Corrupted single-file entry '${entry.RelativePath}'. Declared decompressed size '${entry.Size}' is not the same as actual decompressed size '${decompressedStream.Length}'.");
|
||||
}
|
||||
|
||||
decompressedStream.Seek(0, SeekOrigin.Begin);
|
||||
contents = decompressedStream;
|
||||
}
|
||||
|
||||
using (var fileStream = File.Create(Path.Combine(outputDirectory, entry.RelativePath))) {
|
||||
contents.CopyTo(fileStream);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GenerateBundle(Bundler bundler, string sourceDir, string outputDir)
|
||||
{
|
||||
// Convert sourceDir to absolute path
|
||||
sourceDir = Path.GetFullPath(sourceDir);
|
||||
|
||||
// Get all files in the source directory and all sub-directories.
|
||||
string[] sources = Directory.GetFiles(sourceDir, searchPattern: "*", searchOption: SearchOption.AllDirectories);
|
||||
|
||||
// Sort the file names to keep the bundle construction deterministic.
|
||||
Array.Sort(sources, StringComparer.Ordinal);
|
||||
|
||||
List<FileSpec> fileSpecs = new List<FileSpec>(sources.Length);
|
||||
foreach (var file in sources) {
|
||||
fileSpecs.Add(new FileSpec(file, Path.GetRelativePath(sourceDir, file)));
|
||||
}
|
||||
|
||||
return bundler.GenerateBundle(fileSpecs);
|
||||
}
|
||||
|
||||
public struct Header
|
||||
{
|
||||
public uint MajorVersion;
|
||||
public uint MinorVersion;
|
||||
public int FileCount;
|
||||
public string BundleID;
|
||||
|
||||
// Fields introduced with v2:
|
||||
public long DepsJsonOffset;
|
||||
public long DepsJsonSize;
|
||||
public long RuntimeConfigJsonOffset;
|
||||
public long RuntimeConfigJsonSize;
|
||||
public ulong Flags;
|
||||
|
||||
public ImmutableArray<Entry> Entries;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// FileType: Identifies the type of file embedded into the bundle.
|
||||
///
|
||||
/// The bundler differentiates a few kinds of files via the manifest,
|
||||
/// with respect to the way in which they'll be used by the runtime.
|
||||
/// </summary>
|
||||
public enum FileType : byte
|
||||
{
|
||||
Unknown, // Type not determined.
|
||||
Assembly, // IL and R2R Assemblies
|
||||
NativeBinary, // NativeBinaries
|
||||
DepsJson, // .deps.json configuration file
|
||||
RuntimeConfigJson, // .runtimeconfig.json configuration file
|
||||
Symbols // PDB Files
|
||||
};
|
||||
|
||||
public struct Entry
|
||||
{
|
||||
public long Offset;
|
||||
public long Size;
|
||||
public long CompressedSize; // 0 if not compressed, otherwise the compressed size in the bundle
|
||||
public FileType Type;
|
||||
public string RelativePath; // Path of an embedded file, relative to the Bundle source-directory.
|
||||
}
|
||||
|
||||
static UnmanagedMemoryStream AsStream(MemoryMappedViewAccessor view)
|
||||
{
|
||||
long size = checked((long) view.SafeMemoryMappedViewHandle.ByteLength);
|
||||
return new UnmanagedMemoryStream(view.SafeMemoryMappedViewHandle, 0, size);
|
||||
}
|
||||
|
||||
public static Header ReadManifest(MemoryMappedViewAccessor view, long bundleHeaderOffset)
|
||||
{
|
||||
using var stream = AsStream(view);
|
||||
stream.Seek(bundleHeaderOffset, SeekOrigin.Begin);
|
||||
return ReadManifest(stream);
|
||||
}
|
||||
|
||||
public static Header ReadManifest(Stream stream)
|
||||
{
|
||||
var header = new Header();
|
||||
using var reader = new BinaryReader(stream, Encoding.UTF8, leaveOpen: true);
|
||||
header.MajorVersion = reader.ReadUInt32();
|
||||
header.MinorVersion = reader.ReadUInt32();
|
||||
|
||||
// Major versions 3, 4 and 5 were skipped to align bundle versioning with .NET versioning scheme
|
||||
if (header.MajorVersion < 1 || header.MajorVersion > 6) {
|
||||
throw new InvalidDataException($"Unsupported manifest version: {header.MajorVersion}.{header.MinorVersion}");
|
||||
}
|
||||
header.FileCount = reader.ReadInt32();
|
||||
header.BundleID = reader.ReadString();
|
||||
if (header.MajorVersion >= 2) {
|
||||
header.DepsJsonOffset = reader.ReadInt64();
|
||||
header.DepsJsonSize = reader.ReadInt64();
|
||||
header.RuntimeConfigJsonOffset = reader.ReadInt64();
|
||||
header.RuntimeConfigJsonSize = reader.ReadInt64();
|
||||
header.Flags = reader.ReadUInt64();
|
||||
}
|
||||
var entries = ImmutableArray.CreateBuilder<Entry>(header.FileCount);
|
||||
for (int i = 0; i < header.FileCount; i++) {
|
||||
entries.Add(ReadEntry(reader, header.MajorVersion));
|
||||
}
|
||||
header.Entries = entries.MoveToImmutable();
|
||||
return header;
|
||||
}
|
||||
|
||||
private static Entry ReadEntry(BinaryReader reader, uint bundleMajorVersion)
|
||||
{
|
||||
Entry entry;
|
||||
entry.Offset = reader.ReadInt64();
|
||||
entry.Size = reader.ReadInt64();
|
||||
entry.CompressedSize = bundleMajorVersion >= 6 ? reader.ReadInt64() : 0;
|
||||
entry.Type = (FileType) reader.ReadByte();
|
||||
entry.RelativePath = reader.ReadString();
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
@@ -29,4 +29,11 @@
|
||||
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- Requires SDK 6.0.100, used for re-compiling Update.exe with a new icon. I think it's important to take this from the SDK -->
|
||||
<!-- during build, so the bundled version of the singlefilehost matches the one used by msbuild when it first created Update.exe -->
|
||||
<Reference Include="C:\Program Files\dotnet\sdk\6.0.100\Microsoft.NET.HostModel.dll" />
|
||||
<EmbeddedResource Include="C:\Program Files\dotnet\packs\Microsoft.NETCore.App.Host.win-x86\6.0.0\runtimes\win-x86\native\singlefilehost.exe" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
@@ -6,7 +6,6 @@
|
||||
<LangVersion>9</LangVersion>
|
||||
<RootNamespace>Squirrel.Update</RootNamespace>
|
||||
<ApplicationManifest>app.manifest</ApplicationManifest>
|
||||
<ApplicationIcon>..\..\squirrel.ico</ApplicationIcon>
|
||||
<NoWarn>CA1416</NoWarn>
|
||||
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
|
||||
<SignAssembly>True</SignAssembly>
|
||||
@@ -16,11 +15,6 @@
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- If update.ico exists in project directory it will be used instead of the squirrel icon -->
|
||||
<PropertyGroup Condition="Exists('update.ico')">
|
||||
<ApplicationIcon>update.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Squirrel\Squirrel.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
Reference in New Issue
Block a user