Add macos command line bundler

This commit is contained in:
Caelan Sayler
2022-05-06 17:50:31 +01:00
parent 414a27a84c
commit dc218a068f
19 changed files with 749 additions and 150 deletions

View File

@@ -26,6 +26,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Squirrel.CommandLine", "src
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "StubExecutable", "src\StubExecutable\StubExecutable.vcxproj", "{611A03D4-4CDE-4DA0-B151-DE6FAFFB8B8C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel.CommandLine.OSX", "src\Squirrel.CommandLine.OSX\Squirrel.CommandLine.OSX.csproj", "{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
CIBuild|Any CPU = CIBuild|Any CPU
@@ -390,6 +392,54 @@ Global
{611A03D4-4CDE-4DA0-B151-DE6FAFFB8B8C}.Release|x64.Build.0 = Release|x64
{611A03D4-4CDE-4DA0-B151-DE6FAFFB8B8C}.Release|x86.ActiveCfg = Release|Win32
{611A03D4-4CDE-4DA0-B151-DE6FAFFB8B8C}.Release|x86.Build.0 = Release|Win32
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.CIBuild|Any CPU.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.CIBuild|Any CPU.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.CIBuild|Mixed Platforms.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.CIBuild|Mixed Platforms.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.CIBuild|x64.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.CIBuild|x64.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.CIBuild|x86.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.CIBuild|x86.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Coverage|Any CPU.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Coverage|Any CPU.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Coverage|Mixed Platforms.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Coverage|Mixed Platforms.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Coverage|x64.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Coverage|x64.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Coverage|x86.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Coverage|x86.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Debug|x64.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Debug|x64.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Debug|x86.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Debug|x86.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Debug|Any CPU.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Debug|Any CPU.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Debug|Mixed Platforms.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Debug|x64.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Debug|x64.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Debug|x86.ActiveCfg = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Debug|x86.Build.0 = Debug|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Release|Any CPU.ActiveCfg = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Release|Any CPU.Build.0 = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Release|Mixed Platforms.Build.0 = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Release|x64.ActiveCfg = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Release|x64.Build.0 = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Release|x86.ActiveCfg = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Mono Release|x86.Build.0 = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Release|Any CPU.Build.0 = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Release|x64.ActiveCfg = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Release|x64.Build.0 = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Release|x86.ActiveCfg = Release|Any CPU
{5ECCE39A-56E3-4EA8-8F55-CE59979B6B6B}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Squirrel.CommandLine
{
internal class AppInfo
{
public string CFBundleName { get; set; }
public string CFBundleDisplayName { get; set; }
public string CFBundleIdentifier { get; set; }
public string CFBundleVersion { get; set; }
public string CFBundlePackageType { get; set; }
public string CFBundleSignature { get; set; }
public string CFBundleExecutable { get; set; }
public string CFBundleIconFile { get; set; }
public string CFBundleShortVersionString { get; set; }
public string NSPrincipalClass { get; set; }
public bool NSHighResolutionCapable { get; set; }
public bool? NSRequiresAquaSystemAppearance { get; private set; }
}
}

View File

@@ -0,0 +1,73 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.NET.HostModel.AppHost;
namespace Squirrel.CommandLine
{
internal class BundleOptions : ValidatedOptionSet
{
public string packId { get; private set; }
public string packTitle { get; private set; }
public string packVersion { get; private set; }
public string packDirectory { get; private set; }
public string outputDirectory { get; private set; }
public string icon { get; private set; }
public string exeName { get; private set; }
public BundleOptions()
{
Add("o=|outputDir=", "The {DIRECTORY} to create the bundle", v => outputDirectory = v);
Add("e=|exeName=", "The file name of the main executable", v => exeName = v);
Add("u=|packId=", "Unique {ID} for bundle", v => packId = v);
Add("v=|packVersion=", "Current {VERSION} for bundle", v => packVersion = v);
Add("p=|packDir=", "{DIRECTORY} containing application files for bundle", v => packDirectory = v);
Add("packTitle=", "Optional display/friendly {NAME} for bundle", v => packTitle = v);
Add("i=|icon=", "{PATH} to the .icns file for this bundle", v => icon = v);
}
public override void Validate()
{
IsRequired(nameof(packId), nameof(packVersion), nameof(packDirectory));
Squirrel.NuGet.NugetUtil.ThrowIfInvalidNugetId(packId);
Squirrel.NuGet.NugetUtil.ThrowIfVersionNotSemverCompliant(packVersion, false);
IsValidDirectory(nameof(packDirectory), true);
IsRequired(nameof(icon));
IsValidFile(nameof(icon), ".icns");
var exe = Path.Combine(packDirectory, exeName);
if (!File.Exists(exe)) // || !MachOUtils.IsMachOImage(exe))
throw new OptionValidationException($"Could not find mach-o executable at '{exe}'.");
}
}
internal class PackOptions : BaseOptions
{
public string package { get; set; }
public bool noDelta { get; private set; }
public string baseUrl { get; private set; }
public bool includePdb { get; private set; }
public string releaseNotes { get; private set; }
public PackOptions()
{
Add("b=|baseUrl=", "Provides a base URL to prefix the RELEASES file packages with", v => baseUrl = v, true);
Add("p=|package=", "{PATH} to a '.app' directory to releasify", v => package = v);
Add("noDelta", "Skip the generation of delta packages", v => noDelta = true);
Add("includePdb", "Add *.pdb files to bundle", v => includePdb = true);
Add("releaseNotes=", "{PATH} to file with markdown notes for version", v => releaseNotes = v);
}
public override void Validate()
{
IsRequired(nameof(package));
IsValidDirectory(nameof(package), true);
if (!Utility.PathPartEndsWith(package, ".app"))
throw new OptionValidationException("-p argument must point to a macos bundle directory ending in '.app'.");
}
}
}

View File

@@ -0,0 +1,151 @@
// https://raw.githubusercontent.com/egramtel/dotnet-bundle/master/DotNet.Bundle/PlistWriter.cs
using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Xml;
using Squirrel.SimpleSplat;
namespace Squirrel.CommandLine
{
internal class PlistWriter : IEnableLogger
{
private readonly AppInfo _task;
private readonly string _outputDir;
private static readonly string[] ArrayTypeProperties = { "CFBundleURLSchemes" };
private const char Separator = ';';
public const string PlistFileName = "Info.plist";
public PlistWriter(AppInfo task, string outputDir)
{
_task = task;
_outputDir = outputDir;
}
public void Write()
{
var settings = new XmlWriterSettings {
Indent = true,
NewLineOnAttributes = false
};
var path = Path.Combine(_outputDir, PlistFileName);
this.Log().Info($"Writing property list file: {path}");
using (var xmlWriter = XmlWriter.Create(path, settings)) {
xmlWriter.WriteStartDocument();
xmlWriter.WriteRaw(Environment.NewLine);
xmlWriter.WriteRaw(
"<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">");
xmlWriter.WriteRaw(Environment.NewLine);
xmlWriter.WriteStartElement("plist");
xmlWriter.WriteAttributeString("version", "1.0");
xmlWriter.WriteStartElement("dict");
WriteProperty(xmlWriter, nameof(_task.CFBundleName), _task.CFBundleName);
WriteProperty(xmlWriter, nameof(_task.CFBundleDisplayName), _task.CFBundleDisplayName);
WriteProperty(xmlWriter, nameof(_task.CFBundleIdentifier), _task.CFBundleIdentifier);
WriteProperty(xmlWriter, nameof(_task.CFBundleVersion), _task.CFBundleVersion);
WriteProperty(xmlWriter, nameof(_task.CFBundlePackageType), _task.CFBundlePackageType);
WriteProperty(xmlWriter, nameof(_task.CFBundleSignature), _task.CFBundleSignature);
WriteProperty(xmlWriter, nameof(_task.CFBundleExecutable), _task.CFBundleExecutable);
WriteProperty(xmlWriter, nameof(_task.CFBundleIconFile), Path.GetFileName(_task.CFBundleIconFile));
WriteProperty(xmlWriter, nameof(_task.CFBundleShortVersionString), _task.CFBundleShortVersionString);
WriteProperty(xmlWriter, nameof(_task.NSPrincipalClass), _task.NSPrincipalClass);
WriteProperty(xmlWriter, nameof(_task.NSHighResolutionCapable), _task.NSHighResolutionCapable);
if (_task.NSRequiresAquaSystemAppearance.HasValue) {
WriteProperty(xmlWriter, nameof(_task.NSRequiresAquaSystemAppearance), _task.NSRequiresAquaSystemAppearance.Value);
}
//if (_task.CFBundleURLTypes.Length != 0) {
// WriteProperty(xmlWriter, nameof(_task.CFBundleURLTypes), _task.CFBundleURLTypes);
//}
xmlWriter.WriteEndElement();
xmlWriter.WriteEndElement();
}
}
private void WriteProperty(XmlWriter xmlWriter, string name, string value)
{
if (!string.IsNullOrWhiteSpace(value)) {
xmlWriter.WriteStartElement("key");
xmlWriter.WriteString(name);
xmlWriter.WriteEndElement();
xmlWriter.WriteStartElement("string");
xmlWriter.WriteString(value);
xmlWriter.WriteEndElement();
}
}
private void WriteProperty(XmlWriter xmlWriter, string name, bool value)
{
xmlWriter.WriteStartElement("key");
xmlWriter.WriteString(name);
xmlWriter.WriteEndElement();
if (value) {
xmlWriter.WriteStartElement("true");
} else {
xmlWriter.WriteStartElement("false");
}
xmlWriter.WriteEndElement();
}
private void WriteProperty(XmlWriter xmlWriter, string name, string[] values)
{
if (values.Length != 0) {
xmlWriter.WriteStartElement("key");
xmlWriter.WriteString(name);
xmlWriter.WriteEndElement();
xmlWriter.WriteStartElement("array");
foreach (var value in values) {
if (!string.IsNullOrEmpty(value)) {
xmlWriter.WriteStartElement("string");
xmlWriter.WriteString(value);
xmlWriter.WriteEndElement();
}
}
xmlWriter.WriteEndElement();
}
}
//private void WriteProperty(XmlWriter xmlWriter, string name, ITaskItem[] values)
//{
// xmlWriter.WriteStartElement("key");
// xmlWriter.WriteString(name);
// xmlWriter.WriteEndElement();
// xmlWriter.WriteStartElement("array");
// foreach (var value in values) {
// xmlWriter.WriteStartElement("dict");
// var metadataDictionary = value.CloneCustomMetadata();
// foreach (DictionaryEntry entry in metadataDictionary) {
// var dictValue = entry.Value.ToString();
// var dictKey = entry.Key.ToString();
// if (dictValue.Contains(Separator.ToString()) || ArrayTypeProperties.Contains(dictKey)) //array
// {
// WriteProperty(xmlWriter, dictKey, dictValue.Split(Separator));
// } else {
// WriteProperty(xmlWriter, dictKey, dictValue);
// }
// }
// xmlWriter.WriteEndElement(); //End dict
// }
// xmlWriter.WriteEndElement(); //End outside array
//}
}
}

View File

@@ -0,0 +1,133 @@
// See https://aka.ms/new-console-template for more information
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using Claunia.PropertyList;
using Squirrel.SimpleSplat;
namespace Squirrel.CommandLine
{
class Program
{
static IFullLogger Log => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(Program));
static string TempDir => Utility.GetDefaultTempDirectory(null);
public static int Main(string[] args)
{
var commands = new CommandSet {
"[ Package Authoring ]",
{ "bundle", "Reads a build directory and creates a OSX '.app' bundle", new BundleOptions(), Bundle },
{ "pack", "Convert a '.app' bundle into a Squirrel release", new PackOptions(), Pack },
};
return SquirrelHost.Run(args, commands);
}
private static void Pack(PackOptions options)
{
var targetDir = options.releaseDir ?? Path.Combine(".", "Releases");
var di = new DirectoryInfo(targetDir);
if (!Directory.Exists(targetDir)) {
Directory.CreateDirectory(targetDir);
}
//var builder = new StructureBuilder(options.package);
var plistPath = Path.Combine(options.package, "Contents", PlistWriter.PlistFileName);
NSDictionary plist = (NSDictionary) PropertyListParser.Parse(plistPath);
var _ = Utility.GetTempDir(TempDir, out var tmp);
var packId = plist.ObjectForKey("CFBundleIdentifier").ToString();
var packVersion = plist.ObjectForKey("CFBundleVersion").ToString();
var packTitle = plist.ObjectForKey("CFBundleName").ToString();
var nupkgPath = NugetConsole.CreatePackageFromMetadata(
tmp, options.package, packId, packTitle, packTitle,
packVersion, options.releaseNotes, options.includePdb);
var releaseFilePath = Path.Combine(di.FullName, "RELEASES");
var previousReleases = new List<ReleaseEntry>();
if (File.Exists(releaseFilePath)) {
previousReleases.AddRange(ReleaseEntry.ParseReleaseFile(File.ReadAllText(releaseFilePath, Encoding.UTF8)));
}
var rp = new ReleasePackageBuilder(nupkgPath);
//var newPkgPath = rp.CreateReleasePackage(TempDir, Path.Combine(options.releaseDir, rp.SuggestedReleaseFileName), contentsPostProcessHook: (pkgPath, zpkg) => {
// var nuspecPath = Directory.GetFiles(pkgPath, "*.nuspec", SearchOption.TopDirectoryOnly)
// .ContextualSingle("package", "*.nuspec", "top level directory");
// var libDir = Directory.GetDirectories(Path.Combine(pkgPath, "lib"))
// .ContextualSingle("package", "'lib' folder");
// var contentsDir = Path.Combine(libDir, "Contents");
// File.Copy(nuspecPath, Path.Combine(contentsDir, "current.version"));
//});
// we are not currently making any modifications to the package
// so we can just copy it to the right place. uncomment the above otherwise.
var newPkgPath = Path.Combine(targetDir, rp.SuggestedReleaseFileName);
File.Move(rp.InputPackageFile, newPkgPath);
var prev = ReleasePackageBuilder.GetPreviousRelease(previousReleases, rp, targetDir);
if (prev != null && !options.noDelta) {
var deltaBuilder = new DeltaPackageBuilder();
var dp = deltaBuilder.CreateDeltaPackage(prev, rp,
Path.Combine(di.FullName, rp.SuggestedReleaseFileName.Replace("full", "delta")), TempDir);
}
ReleaseEntry.WriteReleaseFile(previousReleases.Concat(new [] { ReleaseEntry.GenerateFromFile(newPkgPath) }), releaseFilePath);
Log.Info("Done");
}
private static void Bundle(BundleOptions options)
{
var info = new AppInfo {
CFBundleName = options.packTitle ?? options.packId,
CFBundleDisplayName = options.packTitle ?? options.packId,
CFBundleExecutable = options.exeName,
CFBundleIdentifier = options.packId,
CFBundlePackageType = "APPL",
CFBundleShortVersionString = options.packVersion,
CFBundleVersion = options.packVersion,
CFBundleSignature = "????",
NSPrincipalClass = "NSApplication",
NSHighResolutionCapable = true,
CFBundleIconFile = Path.GetFileName(options.icon),
};
Log.Info("Creating .app directory structure");
var builder = new StructureBuilder(options.packId, options.outputDirectory);
builder.Build();
Log.Info("Writing Info.plist");
var plist = new PlistWriter(info, builder.ContentsDirectory);
plist.Write();
Log.Info("Copying resources");
File.Copy(options.icon, Path.Combine(builder.ResourcesDirectory, Path.GetFileName(options.icon)));
Log.Info("Copying application files");
CopyFiles(new DirectoryInfo(options.packDirectory), new DirectoryInfo(builder.MacosDirectory));
Log.Info("MacOS application bundle (.app) created at: " + builder.AppDirectory);
Log.Info("Done.");
}
private static void CopyFiles(DirectoryInfo source, DirectoryInfo target)
{
Directory.CreateDirectory(target.FullName);
foreach (var fileInfo in source.GetFiles()) {
var path = Path.Combine(target.FullName, fileInfo.Name);
fileInfo.CopyTo(path, true);
}
foreach (var sourceSubDir in source.GetDirectories()) {
var targetSubDir = target.CreateSubdirectory(sourceSubDir.Name);
CopyFiles(sourceSubDir, targetSubDir);
}
}
}
}

View File

@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<RootNamespace>Squirrel.CommandLine</RootNamespace>
<AssemblyName>SquirrelMac</AssemblyName>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<PublishSingleFile>true</PublishSingleFile>
<PublishTrimmed>true</PublishTrimmed>
<DebugType>embedded</DebugType>
<DebugSymbols>true</DebugSymbols>
<PathMap>$([System.IO.Path]::GetFullPath('$(MSBuildThisFileDirectory)'))=./</PathMap>
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
<SuppressTrimAnalysisWarnings>true</SuppressTrimAnalysisWarnings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="plist-cil" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Squirrel.CommandLine\Squirrel.CommandLine.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,51 @@
// https://github.com/egramtel/dotnet-bundle/blob/master/DotNet.Bundle/StructureBuilder.cs
using System;
using System.IO;
using Squirrel.SimpleSplat;
namespace Squirrel.CommandLine
{
public class StructureBuilder : IEnableLogger
{
private readonly string _id;
private readonly string _outputDir;
private readonly string _appDir;
public StructureBuilder(string appDir)
{
_appDir = appDir;
}
public StructureBuilder(string id, string outputDir)
{
_id = id;
_outputDir = outputDir;
}
public string AppDirectory => _appDir ?? Path.Combine(Path.Combine(_outputDir, _id + ".app"));
public string ContentsDirectory => Path.Combine(AppDirectory, "Contents");
public string MacosDirectory => Path.Combine(ContentsDirectory, "MacOS");
public string ResourcesDirectory => Path.Combine(ContentsDirectory, "Resources");
public void Build()
{
if (string.IsNullOrEmpty(_outputDir))
throw new NotSupportedException();
Directory.CreateDirectory(_outputDir);
if (Directory.Exists(AppDirectory)) {
Directory.Delete(AppDirectory, true);
}
Directory.CreateDirectory(AppDirectory);
Directory.CreateDirectory(ContentsDirectory);
Directory.CreateDirectory(MacosDirectory);
Directory.CreateDirectory(ResourcesDirectory);
}
}
}

View File

@@ -162,7 +162,7 @@ namespace Squirrel.CommandLine
{
IsRequired(nameof(packId), nameof(packVersion), nameof(packDirectory));
Squirrel.NuGet.NugetUtil.ThrowIfInvalidNugetId(packId);
Squirrel.NuGet.NugetUtil.ThrowIfVersionNotSemverCompliant(packVersion);
Squirrel.NuGet.NugetUtil.ThrowIfVersionNotSemverCompliant(packVersion, true);
IsValidDirectory(nameof(packDirectory), true);
IsValidFile(nameof(releaseNotes));
base.ValidateInternal(false);

View File

@@ -8,136 +8,35 @@ using System.Security;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Mono.Options;
using NuGet.Versioning;
using Squirrel.NuGet;
using Squirrel.SimpleSplat;
using Squirrel.CommandLine.Sync;
namespace Squirrel.CommandLine
{
class Program : IEnableLogger
{
#pragma warning disable CS0436 // Type conflicts with imported type
public static string DisplayVersion => ThisAssembly.AssemblyInformationalVersion + (ThisAssembly.IsPublicRelease ? "" : " (prerelease)");
public static string FileVersion => ThisAssembly.AssemblyFileVersion;
#pragma warning restore CS0436 // Type conflicts with imported type
static IFullLogger Log => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(Program));
public static string TempDir => Utility.GetDefaultTempDirectory(null);
static string TempDir => Utility.GetDefaultTempDirectory(null);
public static int Main(string[] args)
{
var logger = new ConsoleLogger();
SquirrelLocator.CurrentMutable.Register(() => logger, typeof(ILogger));
bool help = false;
bool verbose = false;
var globalOptions = new OptionSet() {
{ "h|?|help", "Ignores all other arguments and shows help text", _ => help = true },
{ "verbose", "Print extra diagnostic logging", _ => verbose = true },
};
var exeName = Path.GetFileName(SquirrelRuntimeInfo.EntryExePath);
string sqUsage =
$"Squirrel {DisplayVersion}, tool for creating and deploying Squirrel releases" + Environment.NewLine +
$"Usage: {exeName} [verb] [--option:value]";
var commands = new CommandSet {
"",
sqUsage,
"",
"[ Global Options ]",
globalOptions.GetHelpText().TrimEnd(),
"",
"[ Package Authoring ]",
{ "pack", "Creates a Squirrel release from a folder containing application files", new PackOptions(), Pack },
{ "releasify", "Take an existing nuget package and convert it into a Squirrel release", new ReleasifyOptions(), Releasify },
"",
"[ Package Deployment / Syncing ]",
{ "b2-down", "Download recent releases from BackBlaze B2", new SyncBackblazeOptions(), o => Download(new BackblazeRepository(o)) },
{ "b2-up", "Upload releases to BackBlaze B2", new SyncBackblazeOptions(), o => Upload(new BackblazeRepository(o)) },
{ "http-down", "Download recent releases from an HTTP source", new SyncHttpOptions(), o => Download(new SimpleWebRepository(o)) },
{ "github-down", "Download recent releases from GitHub", new SyncGithubOptions(), o => Download(new GitHubRepository(o)) },
{ "s3-down", "Download recent releases from a S3 bucket", new SyncS3Options(), o => Download(new S3Repository(o)) },
{ "s3-up", "Upload recent releases to a S3 bucket", new SyncS3Options(), o => Upload(new S3Repository(o)) },
//"",
//"[ Examples ]",
//$" {exeName} pack ",
//$" ",
};
try {
globalOptions.Parse(args);
if (verbose) {
logger.Level = LogLevel.Debug;
}
if (help) {
commands.WriteHelp();
return 0;
} else {
// parse cli and run command
commands.Execute(args);
}
return 0;
} catch (Exception ex) when (ex is OptionValidationException || ex is OptionException) {
// if the arguments fail to validate, print argument help
Console.WriteLine();
logger.Write(ex.Message, LogLevel.Error);
commands.WriteHelp();
Console.WriteLine();
logger.Write(ex.Message, LogLevel.Error);
return -1;
} catch (Exception ex) {
// for other errors, just print the error and short usage instructions
Console.WriteLine();
logger.Write(ex.ToString(), LogLevel.Error);
Console.WriteLine();
Console.WriteLine(sqUsage);
Console.WriteLine($" > '{exeName} -h' to see program help.");
return -1;
}
return SquirrelHost.Run(args, commands);
}
static IFullLogger Log => SquirrelLocator.Current.GetService<ILogManager>().GetLogger(typeof(Program));
static void Upload<T>(T repo) where T : IPackageRepository => repo.UploadMissingPackages().Wait();
static void Download<T>(T repo) where T : IPackageRepository => repo.DownloadRecentPackages().Wait();
static void Pack(PackOptions options)
{
var releaseNotesText = String.IsNullOrEmpty(options.releaseNotes)
? "" // no releaseNotes
: $"<releaseNotes>{SecurityElement.Escape(File.ReadAllText(options.releaseNotes))}</releaseNotes>";
using (Utility.GetTempDir(TempDir, out var tmp)) {
string nuspec = $@"
<?xml version=""1.0"" encoding=""utf-8""?>
<package>
<metadata>
<id>{options.packId}</id>
<title>{options.packTitle ?? options.packId}</title>
<description>{options.packTitle ?? options.packId}</description>
<authors>{options.packAuthors ?? options.packId}</authors>
<version>{options.packVersion}</version>
{releaseNotesText}
</metadata>
<files>
<file src=""**"" target=""lib\native\"" exclude=""{(options.includePdb ? "" : "*.pdb;")}*.nupkg;*.vshost.*""/>
</files>
</package>
".Trim();
var nuspecPath = Path.Combine(tmp, options.packId + ".nuspec");
File.WriteAllText(nuspecPath, nuspec);
new NugetConsole().Pack(nuspecPath, options.packDirectory, tmp);
var nupkgPath = Directory.EnumerateFiles(tmp).Where(f => f.EndsWith(".nupkg")).FirstOrDefault();
if (nupkgPath == null)
throw new Exception($"Failed to generate nupkg, unspecified error");
var nupkgPath = NugetConsole.CreatePackageFromMetadata(
tmp, options.packDirectory, options.packId, options.packTitle,
options.packAuthors, options.packVersion, options.releaseNotes, options.includePdb);
options.package = nupkgPath;
Releasify(options);
@@ -237,7 +136,7 @@ namespace Squirrel.CommandLine
// warning if the installed SquirrelLib version is not the same as Squirrel.exe
StringFileInfo sqLib = null;
try {
var myFileVersion = new NuGetVersion(FileVersion).Version;
var myFileVersion = new NuGetVersion(SquirrelHost.FileVersion).Version;
sqLib = Directory.EnumerateFiles(libDir, "SquirrelLib.dll")
.Select(f => { StringFileInfo.ReadVersionInfo(f, out var fi); return fi; })
.FirstOrDefault(fi => fi.FileVersion != myFileVersion);
@@ -247,7 +146,7 @@ namespace Squirrel.CommandLine
if (sqLib != null) {
Log.Warn(
$"SquirrelLib.dll {sqLib.FileVersion} is installed in provided package, " +
$"but current Squirrel.exe version is {DisplayVersion} ({FileVersion}). " +
$"but current Squirrel.exe version is {SquirrelHost.DisplayVersion} ({SquirrelHost.FileVersion}). " +
$"The LIB version and CLI tool version must be the same to build releases " +
$"or the application may fail to update properly.");
}
@@ -457,33 +356,4 @@ namespace Squirrel.CommandLine
}
}
}
class ConsoleLogger : ILogger
{
public LogLevel Level { get; set; } = LogLevel.Info;
private readonly object gate = new object();
private readonly string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
public void Write(string message, LogLevel logLevel)
{
if (logLevel < Level) {
return;
}
message = message.Replace(localAppData, "%localappdata%", StringComparison.InvariantCultureIgnoreCase);
lock (gate) {
string lvl = logLevel.ToString().Substring(0, 4).ToUpper();
if (logLevel == LogLevel.Error || logLevel == LogLevel.Fatal) {
Utility.ConsoleWriteWithColor($"[{lvl}] {message}{Environment.NewLine}", ConsoleColor.Red);
} else if (logLevel == LogLevel.Warn) {
Utility.ConsoleWriteWithColor($"[{lvl}] {message}{Environment.NewLine}", ConsoleColor.Yellow);
} else {
Console.WriteLine($"[{lvl}] {message}");
}
}
}
}
}

View File

@@ -0,0 +1,42 @@
using System;
using Squirrel.SimpleSplat;
namespace Squirrel.CommandLine
{
class ConsoleLogger : ILogger
{
public LogLevel Level { get; set; } = LogLevel.Info;
private readonly object gate = new object();
private readonly string localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
public void Write(string message, LogLevel logLevel)
{
if (logLevel < Level) {
return;
}
if (SquirrelRuntimeInfo.IsWindows)
message = message.Replace(localAppData, "%localappdata%", StringComparison.InvariantCultureIgnoreCase);
lock (gate) {
string lvl = logLevel.ToString().Substring(0, 4).ToUpper();
if (logLevel == LogLevel.Error || logLevel == LogLevel.Fatal) {
Utility.ConsoleWriteWithColor($"[{lvl}] {message}{Environment.NewLine}", ConsoleColor.Red);
} else if (logLevel == LogLevel.Warn) {
Utility.ConsoleWriteWithColor($"[{lvl}] {message}{Environment.NewLine}", ConsoleColor.Yellow);
} else {
Console.WriteLine($"[{lvl}] {message}");
}
}
}
public static ILogger RegisterLogger()
{
var logger = new ConsoleLogger();
SquirrelLocator.CurrentMutable.Register(() => logger, typeof(ILogger));
return logger;
}
}
}

View File

@@ -1,5 +1,7 @@
using System;
using System.IO;
using System.Linq;
using System.Security;
using System.Threading.Tasks;
using NuGet.Commands;
using Squirrel.SimpleSplat;
@@ -9,6 +11,43 @@ namespace Squirrel.CommandLine
{
internal class NugetConsole : NG.ILogger, IEnableLogger
{
public static string CreatePackageFromMetadata(
string tempDir, string packDir, string packId, string packTitle, string packAuthors,
string packVersion, string releaseNotes, bool includePdb)
{
var releaseNotesText = String.IsNullOrEmpty(releaseNotes)
? "" // no releaseNotes
: $"<releaseNotes>{SecurityElement.Escape(File.ReadAllText(releaseNotes))}</releaseNotes>";
string nuspec = $@"
<?xml version=""1.0"" encoding=""utf-8""?>
<package>
<metadata>
<id>{packId}</id>
<title>{packTitle ?? packId}</title>
<description>{packTitle ?? packId}</description>
<authors>{packAuthors ?? packId}</authors>
<version>{packVersion}</version>
{releaseNotesText}
</metadata>
<files>
<file src=""**"" target=""lib\native\"" exclude=""{(includePdb ? "" : "*.pdb;")}*.nupkg;*.vshost.*""/>
</files>
</package>
".Trim();
var nuspecPath = Path.Combine(tempDir, packId + ".nuspec");
File.WriteAllText(nuspecPath, nuspec);
new NugetConsole().Pack(nuspecPath, packDir, tempDir);
var nupkgPath = Directory.EnumerateFiles(tempDir).Where(f => f.EndsWith(".nupkg")).FirstOrDefault();
if (nupkgPath == null)
throw new Exception($"Failed to generate nupkg, unspecified error");
return nupkgPath;
}
public void Pack(string nuspecPath, string baseDirectory, string outputDirectory)
{
this.Log().Info($"Starting to package '{nuspecPath}'");

View File

@@ -2,5 +2,13 @@
using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
[assembly: InternalsVisibleTo("Squirrel.Tests, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("SquirrelMac, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel.CommandLine, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel.CommandLine.OSX, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel.CommandLine.Windows, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Update, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("UpdateMac, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Update.Windows, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Update.OSX, PublicKey=" + SNK.SHA1)]

View File

@@ -42,7 +42,6 @@ namespace Squirrel.CommandLine
public SemanticVersion Version => ReleaseEntry.ParseEntryFileName(InputPackageFile).Version;
[SupportedOSPlatform("windows")]
internal string CreateReleasePackage(string temporaryDirectory, string outputFile, Func<string, string> releaseNotesProcessor = null, Action<string, ZipPackage> contentsPostProcessHook = null)
{
Contract.Requires(!String.IsNullOrEmpty(outputFile));
@@ -62,7 +61,7 @@ namespace Squirrel.CommandLine
// we don't really care that they aren't valid
if (!ModeDetector.InUnitTestRunner()) {
// verify that the .nuspec version is semver compliant
NugetUtil.ThrowIfVersionNotSemverCompliant(package.Version.ToString());
NugetUtil.ThrowIfVersionNotSemverCompliant(package.Version.ToString(), true);
// verify that the suggested filename can be round-tripped as an assurance
// someone won't run across an edge case and install a broken app somehow

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Mono.Options;
using Squirrel.CommandLine.Sync;
using Squirrel.SimpleSplat;
namespace Squirrel.CommandLine
{
internal class SquirrelHost
{
#pragma warning disable CS0436 // Type conflicts with imported type
public static string DisplayVersion => ThisAssembly.AssemblyInformationalVersion + (ThisAssembly.IsPublicRelease ? "" : " (prerelease)");
public static string FileVersion => ThisAssembly.AssemblyFileVersion;
#pragma warning restore CS0436 // Type conflicts with imported type
public static int Run(string[] args, CommandSet packageCommands)
{
var logger = ConsoleLogger.RegisterLogger();
bool help = false;
bool verbose = false;
var globalOptions = new OptionSet() {
{ "h|?|help", "Ignores all other arguments and shows help text", _ => help = true },
{ "verbose", "Print extra diagnostic logging", _ => verbose = true },
};
var exeName = Path.GetFileName(SquirrelRuntimeInfo.EntryExePath);
string sqUsage =
$"Squirrel {DisplayVersion}, tool for creating and deploying Squirrel releases" + Environment.NewLine +
$"Usage: {exeName} [verb] [--option:value]";
var commands = new CommandSet {
"",
sqUsage,
"",
"[ Global Options ]",
globalOptions.GetHelpText().TrimEnd(),
"",
packageCommands,
//"[ Package Authoring ]",
//{ "pack", "Creates a Squirrel release from a folder containing application files", new PackOptions(), Pack },
//{ "releasify", "Take an existing nuget package and convert it into a Squirrel release", new ReleasifyOptions(), Releasify },
"",
"[ Package Deployment / Syncing ]",
{ "b2-down", "Download recent releases from BackBlaze B2", new SyncBackblazeOptions(), o => Download(new BackblazeRepository(o)) },
{ "b2-up", "Upload releases to BackBlaze B2", new SyncBackblazeOptions(), o => Upload(new BackblazeRepository(o)) },
{ "http-down", "Download recent releases from an HTTP source", new SyncHttpOptions(), o => Download(new SimpleWebRepository(o)) },
{ "github-down", "Download recent releases from GitHub", new SyncGithubOptions(), o => Download(new GitHubRepository(o)) },
{ "s3-down", "Download recent releases from a S3 bucket", new SyncS3Options(), o => Download(new S3Repository(o)) },
{ "s3-up", "Upload recent releases to a S3 bucket", new SyncS3Options(), o => Upload(new S3Repository(o)) },
//"",
//"[ Examples ]",
//$" {exeName} pack ",
//$" ",
};
try {
globalOptions.Parse(args);
if (verbose) {
logger.Level = LogLevel.Debug;
}
if (help) {
commands.WriteHelp();
return 0;
} else {
// parse cli and run command
commands.Execute(args);
}
return 0;
} catch (Exception ex) when (ex is OptionValidationException || ex is OptionException) {
// if the arguments fail to validate, print argument help
Console.WriteLine();
logger.Write(ex.Message, LogLevel.Error);
commands.WriteHelp();
Console.WriteLine();
logger.Write(ex.Message, LogLevel.Error);
return -1;
} catch (Exception ex) {
// for other errors, just print the error and short usage instructions
Console.WriteLine();
logger.Write(ex.ToString(), LogLevel.Error);
Console.WriteLine();
Console.WriteLine(sqUsage);
Console.WriteLine($" > '{exeName} -h' to see program help.");
return -1;
}
}
static void Upload<T>(T repo) where T : IPackageRepository => repo.UploadMissingPackages().GetAwaiter().GetResult();
static void Download<T>(T repo) where T : IPackageRepository => repo.DownloadRecentPackages().GetAwaiter().GetResult();
}
}

View File

@@ -185,6 +185,11 @@ namespace Squirrel.CommandLine
this.Add(new CommandAction<T>(command, description, options, action));
}
public void Add(CommandSet commands)
{
AddRange(commands);
}
public virtual void Execute(string[] args)
{
if (args.Length == 0)

View File

@@ -94,7 +94,7 @@ namespace Squirrel
public static bool PathPartEndsWith(string part1, string endsWith)
{
return part1.StartsWith(endsWith, SquirrelRuntimeInfo.PathStringComparison);
return part1.EndsWith(endsWith, SquirrelRuntimeInfo.PathStringComparison);
}
public static bool FileHasExtension(string filePath, string extension)
@@ -378,19 +378,26 @@ namespace Squirrel
}));
}
[SupportedOSPlatform("windows")]
public static string GetDefaultTempDirectory(string localAppDirectory)
{
string tempDir;
if (SquirrelRuntimeInfo.IsOSX) {
tempDir = "/tmp/squirrel";
} else if (SquirrelRuntimeInfo.IsWindows) {
#if DEBUG
const string TEMP_ENV_VAR = "CLOWD_SQUIRREL_TEMP_DEBUG";
const string TEMP_DIR_NAME = "SquirrelClowdTempDebug";
const string TEMP_ENV_VAR = "CLOWD_SQUIRREL_TEMP_DEBUG";
const string TEMP_DIR_NAME = "SquirrelClowdTempDebug";
#else
const string TEMP_ENV_VAR = "CLOWD_SQUIRREL_TEMP";
const string TEMP_DIR_NAME = "SquirrelClowdTemp";
const string TEMP_ENV_VAR = "CLOWD_SQUIRREL_TEMP";
const string TEMP_DIR_NAME = "SquirrelClowdTemp";
#endif
var tempDir = Environment.GetEnvironmentVariable(TEMP_ENV_VAR);
tempDir = tempDir ?? Path.Combine(localAppDirectory ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), TEMP_DIR_NAME);
tempDir = Environment.GetEnvironmentVariable(TEMP_ENV_VAR);
tempDir = tempDir ?? Path.Combine(localAppDirectory ?? Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), TEMP_DIR_NAME);
} else {
throw new NotSupportedException();
}
var di = new DirectoryInfo(tempDir);
if (!di.Exists) di.Create();

View File

@@ -27,12 +27,16 @@ namespace Squirrel.NuGet
throw new ArgumentException($"Invalid package Id '{id}', it must contain only alphanumeric characters, underscores, dashes, and dots.");
}
public static void ThrowIfVersionNotSemverCompliant(string version)
public static void ThrowIfVersionNotSemverCompliant(string version, bool allowTags)
{
if (SemanticVersion.TryParse(version, out var parsed)) {
if (parsed < new SemanticVersion(0, 0, 1)) {
throw new Exception($"Invalid package version '{version}', it must be >= 0.0.1.");
}
if (!allowTags && (parsed.HasMetadata || parsed.IsPrerelease)) {
throw new Exception($"Invalid package version '{version}', metadata/pre-release tags are not permitted.");
}
} else {
throw new Exception($"Invalid package version '{version}', it must be a 3-part SemVer2 compliant version string.");
}

View File

@@ -4,10 +4,14 @@ using System.Runtime.InteropServices;
[assembly: ComVisible(false)]
[assembly: InternalsVisibleTo("Squirrel.Tests, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("SquirrelMac, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel.CommandLine, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel.CommandLine.OSX, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Squirrel.CommandLine.Windows, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Update, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("UpdateMac, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Update.Windows, PublicKey=" + SNK.SHA1)]
[assembly: InternalsVisibleTo("Update.OSX, PublicKey=" + SNK.SHA1)]
internal static class SNK
{

View File

@@ -17,6 +17,7 @@ using InteropArchitecture = System.Runtime.InteropServices.Architecture;
namespace System.Runtime.Versioning
{
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
internal class SupportedOSPlatformGuardAttribute : Attribute
{
public SupportedOSPlatformGuardAttribute(string platformName) { }
@@ -29,6 +30,7 @@ namespace System.Runtime.Versioning
namespace System.Runtime.Versioning
{
[AttributeUsage(AttributeTargets.All, Inherited = false, AllowMultiple = true)]
internal class SupportedOSPlatformAttribute : Attribute
{
public SupportedOSPlatformAttribute(string platformName) { }