Skip to content
This repository has been archived by the owner on Jul 5, 2024. It is now read-only.

Commit

Permalink
Store package architecture and runtime dependencies in release nuspec
Browse files Browse the repository at this point in the history
  • Loading branch information
caesay committed Jan 30, 2022
1 parent 08ebbed commit 9d05773
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 44 deletions.
2 changes: 1 addition & 1 deletion src/Squirrel/AssemblyRuntimeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private static void CheckArchitectureWindows()

try {
if (IsWow64Process2(GetCurrentProcess(), out var _, out var nativeMachine)) {
if (Utility.TryParseEnum<RuntimeCpu>(nativeMachine, out var val)) {
if (Utility.TryParseEnumU16<RuntimeCpu>(nativeMachine, out var val)) {
Architecture = val;
}
}
Expand Down
7 changes: 3 additions & 4 deletions src/Squirrel/Internal/ReleasePackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,9 @@ internal string CreateReleasePackage(string outputFile, Func<string, string> rel
}
}

// we can tell from here what platform(s) the package targets
// but given this is a simple package we only
// ever expect one entry here (crash hard otherwise)
var frameworks = package.GetFrameworks();
// we can tell from here what platform(s) the package targets but given this is a
// simple package we only ever expect one entry here (crash hard otherwise)
var frameworks = package.Frameworks;
if (frameworks.Count() > 1) {
var platforms = frameworks
.Aggregate(new StringBuilder(), (sb, f) => sb.Append(f.ToString() + "; "));
Expand Down
10 changes: 10 additions & 0 deletions src/Squirrel/Internal/Utility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,16 @@ public static string RemoveByteOrderMarkerIfPresent(byte[] content)
return Encoding.UTF8.GetString(output);
}

public static bool TryParseEnumU16<TEnum>(ushort enumValue, out TEnum retVal)
{
retVal = default(TEnum);
bool success = Enum.IsDefined(typeof(TEnum), enumValue);
if (success) {
retVal = (TEnum) Enum.ToObject(typeof(TEnum), enumValue);
}
return success;
}

public static string NormalizePath(string path)
{
var fullPath = Path.GetFullPath(path);
Expand Down
98 changes: 68 additions & 30 deletions src/Squirrel/NuGet/ZipPackage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Globalization;
using System.IO;
using System.Linq;
using System.Xml;
using System.Xml.Linq;
using SharpCompress.Archives.Zip;

Expand All @@ -17,13 +18,15 @@ internal interface IPackage
string ProductCopyright { get; }
string Language { get; }
SemanticVersion Version { get; }
IEnumerable<string> Frameworks { get; }
IEnumerable<FrameworkAssemblyReference> FrameworkAssemblies { get; }
IEnumerable<PackageDependencySet> DependencySets { get; }
Uri ProjectUrl { get; }
string ReleaseNotes { get; }
Uri IconUrl { get; }
string[] GetFrameworks();
Stream ReadLibFileStream(string fileName);
IEnumerable<string> Tags { get; }
IEnumerable<string> RuntimeDependencies { get; }
RuntimeCpu MachineArchitecture { get; }
}

internal class ZipPackage : IPackage
Expand All @@ -41,6 +44,10 @@ internal class ZipPackage : IPackage
public string ReleaseNotes { get; private set; }
public Uri IconUrl { get; private set; }
public string Language { get; private set; }
public IEnumerable<string> Tags { get; private set; } = Enumerable.Empty<string>();
public IEnumerable<string> RuntimeDependencies { get; private set; } = Enumerable.Empty<string>();
public IEnumerable<string> Frameworks { get; private set; }
public RuntimeCpu MachineArchitecture { get; private set; }

protected string Description { get; private set; }
protected IEnumerable<string> Authors { get; private set; } = Enumerable.Empty<string>();
Expand All @@ -59,14 +66,16 @@ public ZipPackage(string filePath)
}

_streamFactory = () => File.OpenRead(filePath);
EnsureManifest();
}

public string[] GetFrameworks()
{
using var stream = _streamFactory();
using var zip = ZipArchive.Open(stream);
using var manifest = GetManifestEntry(zip).OpenEntryStream();
ReadManifest(manifest);
Frameworks = GetFrameworks(zip);
}

private string[] GetFrameworks(ZipArchive zip)
{
var fileFrameworks = GetPackageFiles(zip)
.Select(z => NugetUtil.ParseFrameworkNameFromFilePath(z.Path, out var _));

Expand Down Expand Up @@ -134,19 +143,15 @@ where IsPackageFile(path)
select (path, entry);
}

private void EnsureManifest()
private ZipArchiveEntry GetManifestEntry(ZipArchive zip)
{
using var stream = _streamFactory();
using var zip = ZipArchive.Open(stream);

var manifest = zip.Entries
.FirstOrDefault(f => f.Key.EndsWith(NugetUtil.ManifestExtension, StringComparison.OrdinalIgnoreCase));
.FirstOrDefault(f => f.Key.EndsWith(NugetUtil.ManifestExtension, StringComparison.OrdinalIgnoreCase));

if (manifest == null)
throw new InvalidOperationException("PackageDoesNotContainManifest");
throw new InvalidDataException("PackageDoesNotContainManifest");

using var manifestStream = manifest.OpenEntryStream();
ReadManifest(manifestStream);
return manifest;
}

private void ReadManifest(Stream manifestStream)
Expand All @@ -171,6 +176,35 @@ private void ReadManifest(Stream manifestStream)
}
}

public static void SetSquirrelMetadata(string nuspecPath, RuntimeCpu architecture, IEnumerable<string> runtimes)
{
Dictionary<string, string> toSet = new();
if (architecture != RuntimeCpu.Unknown)
toSet.Add("machineArchitecture", architecture.ToString());
if (runtimes.Any())
toSet.Add("runtimeDependencies", String.Join(", ", runtimes));

if (!toSet.Any())
return;

XDocument document;
using (var fs = File.OpenRead(nuspecPath))
document = NugetUtil.LoadSafe(fs, ignoreWhiteSpace: true);

var metadataElement = document.Root.ElementsNoNamespace("metadata").FirstOrDefault();
if (metadataElement == null) {
throw new InvalidDataException(
String.Format(CultureInfo.CurrentCulture, "Manifest_RequiredElementMissing", "metadata"));
}

foreach (var el in toSet) {
var elName = XName.Get(el.Key, document.Root.GetDefaultNamespace().NamespaceName);
metadataElement.SetElementValue(elName, el.Value);
}

document.Save(nuspecPath);
}

private void ReadMetadataValue(XElement element, HashSet<string> allElements)
{
if (element.Value == null) {
Expand All @@ -179,6 +213,12 @@ private void ReadMetadataValue(XElement element, HashSet<string> allElements)

allElements.Add(element.Name.LocalName);

IEnumerable<string> getCommaDelimitedValue(string v)
{
return v?.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim()) ?? Enumerable.Empty<string>();
}

string value = element.Value.SafeTrim();
switch (element.Name.LocalName) {
case "id":
Expand All @@ -188,26 +228,17 @@ private void ReadMetadataValue(XElement element, HashSet<string> allElements)
Version = new SemanticVersion(value);
break;
case "authors":
Authors = value?.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()) ?? Enumerable.Empty<string>();
Authors = getCommaDelimitedValue(value);
break;
case "owners":
Owners = value;
break;
//case "licenseUrl":
// LicenseUrl = value;
// break;
case "projectUrl":
ProjectUrl = new Uri(value);
break;
case "iconUrl":
IconUrl = new Uri(value);
break;
//case "requireLicenseAcceptance":
// RequireLicenseAcceptance = XmlConvert.ToBoolean(value);
// break;
//case "developmentDependency":
// DevelopmentDependency = XmlConvert.ToBoolean(value);
// break;
case "description":
Description = value;
break;
Expand All @@ -226,18 +257,25 @@ private void ReadMetadataValue(XElement element, HashSet<string> allElements)
case "title":
Title = value;
break;
//case "tags":
// Tags = value;
// break;
case "tags":
Tags = getCommaDelimitedValue(value);
break;
case "dependencies":
DependencySets = ReadDependencySets(element);
break;
case "frameworkAssemblies":
FrameworkAssemblies = ReadFrameworkAssemblies(element);
break;
//case "references":
// ReferenceSets = ReadReferenceSets(element);
// break;

// ===
// the following metadata elements are added by squirrel and are not
// used by nuget.
case "machineArchitecture":
MachineArchitecture = (RuntimeCpu) Enum.Parse(typeof(RuntimeCpu), value, true);
break;
case "runtimeDependencies":
RuntimeDependencies = getCommaDelimitedValue(value);
break;
}
}

Expand Down
45 changes: 36 additions & 9 deletions src/SquirrelCli/Program.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
Expand Down Expand Up @@ -192,19 +192,46 @@ static void Releasify(ReleasifyOptions options)

var rp = new ReleasePackage(file.FullName);
rp.CreateReleasePackage(Path.Combine(di.FullName, rp.SuggestedReleaseFileName), contentsPostProcessHook: (pkgPath, zpkg) => {
var nuspecPath = Directory.GetFiles(pkgPath, "*.nuspec", SearchOption.TopDirectoryOnly).First();
var libDir = Directory.GetDirectories(Path.Combine(pkgPath, "lib")).First();
var awareExes = SquirrelAwareExecutableDetector.GetAllSquirrelAwareApps(libDir);
// unless the validation has been disabled, do not allow the creation of packages without a SquirrelAwareApp inside
if (!options.allowUnaware) {
var awareExes = SquirrelAwareExecutableDetector.GetAllSquirrelAwareApps(libDir);
if (!awareExes.Any()) {
throw new ArgumentException(
"There are no SquirreAwareApp's in the provided package. Please mark an exe " +
"as aware using the assembly manifest, or use the '--allowUnaware' argument " +
"to skip this validation and create a package anyway (at your own risk).");
}
if (!options.allowUnaware && !awareExes.Any()) {
throw new ArgumentException(
"There are no SquirreAwareApp's in the provided package. Please mark an exe " +
"as aware using the assembly manifest, or use the '--allowUnaware' argument " +
"to skip this validation and create a package anyway (at your own risk).");
}
// record architecture of squirrel aware binaries so setup can fast fail if unsupported
var pearchs = awareExes
.Select(path => new PeNet.PeFile(path))
.Select(pe => pe?.ImageNtHeaders?.FileHeader?.Machine ?? 0)
.Select(machine => { Utility.TryParseEnumU16<RuntimeCpu>((ushort) machine, out var cpu); return cpu; })
.Where(m => m != RuntimeCpu.Unknown)
.Distinct()
.ToArray();
if (pearchs.Length > 1) {
Log.Warn("Multiple squirrel aware binaries were detected with different machine architectures: " + String.Join(", ", pearchs));
}
// CS: the first will be selected for the "package" architecture. this order is important,
// because of emulation support. Arm64 generally supports x86/x64 emulation, and x64
// often supports x86 emulation, so we want to pick the least compatible architecture
// for the package.
var archOrder = new[] { RuntimeCpu.Arm64, RuntimeCpu.X64, RuntimeCpu.X86 };
var pkgarch = archOrder.First(o => pearchs.Contains(o));
if (pkgarch == RuntimeCpu.Unknown) {
Log.Warn("Unable to detect package machine architecture. Are the SquirrelAware binaries compatible?");
} else {
Log.Info($"Package architecture: {pkgarch} (implicit, from a SquirrelAware binary)");
}
ZipPackage.SetSquirrelMetadata(nuspecPath, pkgarch, requiredFrameworks);
// create stub executable for all exe's in this package (except Squirrel!)
Log.Info("Creating stub executables");
new DirectoryInfo(pkgPath).GetAllFilesRecursively()
Expand Down
1 change: 1 addition & 0 deletions src/SquirrelCli/SquirrelCli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<PackageReference Include="B2Net" Version="0.7.5" />
<PackageReference Include="System.Drawing.Common" Version="6.0.0" />
<PackageReference Include="NuGet.Commands" Version="6.0.0" />
<PackageReference Include="PeNet" Version="2.9.3" />
</ItemGroup>

<ItemGroup>
Expand Down

0 comments on commit 9d05773

Please sign in to comment.